diff --git a/apps/student/src/androidTest/assets/test.txt b/apps/student/src/androidTest/assets/test.txt new file mode 100644 index 0000000000..9bbedf7fb2 --- /dev/null +++ b/apps/student/src/androidTest/assets/test.txt @@ -0,0 +1 @@ +This is a test file for assignment submission. \ No newline at end of file diff --git a/apps/student/src/androidTest/assets/test_audio.mp3 b/apps/student/src/androidTest/assets/test_audio.mp3 new file mode 100644 index 0000000000..57add90168 Binary files /dev/null and b/apps/student/src/androidTest/assets/test_audio.mp3 differ diff --git a/apps/student/src/androidTest/assets/test_video.mp4 b/apps/student/src/androidTest/assets/test_video.mp4 new file mode 100644 index 0000000000..91352b4b07 Binary files /dev/null and b/apps/student/src/androidTest/assets/test_video.mp4 differ 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 19384887e6..2a7c994f07 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -15,8 +15,21 @@ */ package com.instructure.student.ui.interaction +import android.Manifest +import android.app.Activity +import android.app.Instrumentation +import android.content.Intent +import android.net.Uri +import android.provider.MediaStore import androidx.compose.ui.platform.ComposeView +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiSelector 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 @@ -31,6 +44,7 @@ import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvas.espresso.refresh import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -49,6 +63,7 @@ import dagger.hilt.android.testing.UninstallModules import org.hamcrest.Matchers import org.junit.Assert.assertNotNull import org.junit.Test +import java.io.File import java.util.Calendar @HiltAndroidTest @@ -63,9 +78,7 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_ONLINE_URL) - fun testSubmission_submitAssignment() { - // TODO - Test submitting for each submission type - // For now, I'm going to just test one submission type + fun testSubmission_submitOnlineURL() { val data = MockCanvas.init( studentCount = 1, courseCount = 1 @@ -89,6 +102,246 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentDetailsPage.assertStatusSubmitted() } + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_TEXT_ENTRY) + fun testSubmission_submitTextEntry() { + val data = MockCanvas.init( + studentCount = 1, + courseCount = 1 + ) + + val course = data.courses.values.first() + val student = data.students[0] + val token = data.tokenFor(student)!! + val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)) + data.addSubmissionForAssignment( + assignmentId = assignment.id, + userId = data.users.values.first().id, + type = Assignment.SubmissionType.ONLINE_TEXT_ENTRY.apiString + ) + tokenLogin(data.domain, token, student) + routeTo("courses/${course.id}/assignments", data.domain) + + assignmentListPage.clickAssignment(assignment) + assignmentDetailsPage.clickSubmit() + textSubmissionUploadPage.typeText("This is my test submission text.") + textSubmissionUploadPage.clickOnSubmitButton() + assignmentDetailsPage.assertStatusSubmitted() + } + + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_FILE_UPLOAD) + fun testSubmission_submitFileUpload() { + val data = MockCanvas.init( + studentCount = 1, + courseCount = 1 + ) + + val course = data.courses.values.first() + val student = data.students[0] + val token = data.tokenFor(student)!! + val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_UPLOAD)) + data.addSubmissionForAssignment( + assignmentId = assignment.id, + userId = data.users.values.first().id, + type = Assignment.SubmissionType.ONLINE_UPLOAD.apiString + ) + tokenLogin(data.domain, token, student) + routeTo("courses/${course.id}/assignments", data.domain) + + val fileName = "test.txt" + Intents.init() + try { + stubFilePickerIntent(fileName) + setupFileOnDevice(fileName) + + assignmentListPage.clickAssignment(assignment) + assignmentDetailsPage.clickSubmit() + pickerSubmissionUploadPage.chooseDevice() + pickerSubmissionUploadPage.waitForSubmitButtonToAppear() + pickerSubmissionUploadPage.assertFileDisplayed(fileName) + pickerSubmissionUploadPage.submit() + assignmentDetailsPage.assertStatusSubmitted() + } finally { + Intents.release() + } + } + + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_MEDIA_RECORDING) + fun testSubmission_submitMediaRecordingChooseMediaFile() { + val data = MockCanvas.init( + studentCount = 1, + courseCount = 1 + ) + + val course = data.courses.values.first() + val student = data.students[0] + val token = data.tokenFor(student)!! + val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf(Assignment.SubmissionType.MEDIA_RECORDING)) + data.addSubmissionForAssignment( + assignmentId = assignment.id, + userId = data.users.values.first().id, + type = Assignment.SubmissionType.MEDIA_RECORDING.apiString + ) + tokenLogin(data.domain, token, student) + routeTo("courses/${course.id}/assignments", data.domain) + + val activity = activityRule.activity + grantPermissions(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) + + val fileName = "test_video.mp4" + copyAssetFileToExternalCache(activity, fileName) + + val resultData = Intent() + val dir = activity.externalCacheDir + val file = File(dir?.path, fileName) + val uri = Uri.fromFile(file) + resultData.data = uri + val activityResult = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData) + + Intents.init() + try { + Intents.intending( + Matchers.anyOf( + IntentMatchers.hasAction(Intent.ACTION_GET_CONTENT), + IntentMatchers.hasAction(Intent.ACTION_PICK), + IntentMatchers.hasAction(Intent.ACTION_OPEN_DOCUMENT) + ) + ).respondWith(activityResult) + + assignmentListPage.clickAssignment(assignment) + assignmentDetailsPage.clickSubmit() + + onView(withId(R.id.submissionEntryMediaFile)).perform(click()) + + pickerSubmissionUploadPage.waitForSubmitButtonToAppear() + pickerSubmissionUploadPage.assertFileDisplayed(fileName) + pickerSubmissionUploadPage.submit() + assignmentDetailsPage.assertStatusSubmitted() + } finally { + Intents.release() + } + } + + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_MEDIA_RECORDING) + fun testSubmission_submitMediaRecordingRecordVideo() { + val data = MockCanvas.init( + studentCount = 1, + courseCount = 1 + ) + + val course = data.courses.values.first() + val student = data.students[0] + val token = data.tokenFor(student)!! + val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf(Assignment.SubmissionType.MEDIA_RECORDING)) + data.addSubmissionForAssignment( + assignmentId = assignment.id, + userId = data.users.values.first().id, + type = Assignment.SubmissionType.MEDIA_RECORDING.apiString + ) + tokenLogin(data.domain, token, student) + routeTo("courses/${course.id}/assignments", data.domain) + + val activity = activityRule.activity + grantPermissions(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) + + val testVideoFile = "test_video.mp4" + copyAssetFileToExternalCache(activity, testVideoFile) + + var capturedVideoUri: Uri? = null + + Intents.init() + Intents.intending( + Matchers.allOf( + IntentMatchers.hasAction(MediaStore.ACTION_VIDEO_CAPTURE), + IntentMatchers.hasExtraWithKey(MediaStore.EXTRA_OUTPUT) + ) + ).respondWithFunction { intent -> + val outputUri = intent.extras?.get(MediaStore.EXTRA_OUTPUT) as? Uri + capturedVideoUri = outputUri + if (outputUri != null) { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val dir = context.externalCacheDir + val sampleFile = File(dir, testVideoFile) + if (outputUri.scheme == "file") { + val destFile = File(outputUri.path!!) + destFile.parentFile?.mkdirs() + sampleFile.copyTo(destFile, overwrite = true) + } else if (outputUri.scheme == "content") { + context.contentResolver.openOutputStream(outputUri)?.use { outputStream -> + sampleFile.inputStream().use { inputStream -> + inputStream.copyTo(outputStream) + } + } + } + } + Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()) + } + + assignmentListPage.clickAssignment(assignment) + assignmentDetailsPage.clickSubmit() + onView(withId(R.id.submissionEntryVideo)).perform(click()) + + Intents.release() + + pickerSubmissionUploadPage.waitForSubmitButtonToAppear() + + val fileName = File(capturedVideoUri!!.path!!).name + pickerSubmissionUploadPage.assertFileDisplayed(fileName) + pickerSubmissionUploadPage.submit() + + assignmentDetailsPage.assertStatusSubmitted() + } + + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_MEDIA_RECORDING) + fun testSubmission_submitMediaRecordingRecordAudio() { + val data = MockCanvas.init( + studentCount = 1, + courseCount = 1 + ) + + val course = data.courses.values.first() + val student = data.students[0] + val token = data.tokenFor(student)!! + val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf(Assignment.SubmissionType.MEDIA_RECORDING)) + data.addSubmissionForAssignment( + assignmentId = assignment.id, + userId = data.users.values.first().id, + type = Assignment.SubmissionType.MEDIA_RECORDING.apiString + ) + tokenLogin(data.domain, token, student) + routeTo("courses/${course.id}/assignments", data.domain) + + val activity = activityRule.activity + grantPermissions(Manifest.permission.RECORD_AUDIO) + + val testAudioFileName = "test_audio.mp3" + copyAssetFileToExternalCache(activity, testAudioFileName) + + val context = InstrumentationRegistry.getInstrumentation().targetContext + val recordingFile = File(context.externalCacheDir, "audio.amr") + val testAudioFile = File(context.externalCacheDir, testAudioFileName) + testAudioFile.copyTo(recordingFile, overwrite = true) + + assignmentListPage.clickAssignment(assignment) + assignmentDetailsPage.clickSubmit() + onView(withId(R.id.submissionEntryAudio)).perform(click()) + + device.findObject(UiSelector().resourceIdMatches(".*recordAudioButton")).click() + + testAudioFile.copyTo(recordingFile, overwrite = true) + + device.findObject(UiSelector().resourceIdMatches(".*stopButton")).click() + + device.findObject(UiSelector().resourceIdMatches(".*sendAudioButton")).click() + + refresh() + assignmentDetailsPage.assertStatusSubmitted() + } + @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) fun testSubmissionStatus_NotSubmitted() { @@ -694,6 +947,16 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { return assignment } + private fun grantPermissions(vararg permissions: String) { + val activity = activityRule.activity + permissions.forEach { permission -> + InstrumentationRegistry.getInstrumentation().uiAutomation.grantRuntimePermission( + activity.packageName, + permission + ) + } + } + override fun enableAndConfigureAccessibilityChecks() { extraAccessibilitySupressions = Matchers.allOf( AccessibilityCheckResultUtils.matchesCheck( 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 106fb425bc..e59a21f785 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 @@ -39,7 +39,7 @@ enum class FeatureCategory { enum class SecondaryFeatureCategory { NONE, LOGIN_K5, - SUBMISSIONS_TEXT_ENTRY, SUBMISSIONS_ANNOTATIONS, SUBMISSIONS_ONLINE_URL, SUBMISSIONS_MULTIPLE_TYPE, + SUBMISSIONS_TEXT_ENTRY, SUBMISSIONS_ANNOTATIONS, SUBMISSIONS_ONLINE_URL, SUBMISSIONS_MULTIPLE_TYPE, SUBMISSIONS_FILE_UPLOAD, SUBMISSIONS_MEDIA_RECORDING, ASSIGNMENT_COMMENTS, ASSIGNMENT_QUIZZES, ASSIGNMENT_DISCUSSIONS, HOMEROOM, K5_GRADES, IMPORTANT_DATES, RESOURCES, SCHEDULE, GROUPS_DASHBOARD, GROUPS_FILES, GROUPS_ANNOUNCEMENTS, GROUPS_DISCUSSIONS, GROUPS_PAGES, GROUPS_PEOPLE, EVENTS_DISCUSSIONS, EVENTS_QUIZZES, EVENTS_ASSIGNMENTS, EVENTS_NOTIFICATIONS, SETTINGS_EMAIL_NOTIFICATIONS,