1515 */
1616package com.instructure.student.ui.interaction
1717
18+ import android.Manifest
19+ import android.app.Activity
20+ import android.app.Instrumentation
21+ import android.content.Intent
22+ import android.net.Uri
23+ import android.provider.MediaStore
1824import androidx.compose.ui.platform.ComposeView
25+ import androidx.test.espresso.Espresso.onView
26+ import androidx.test.espresso.action.ViewActions.click
27+ import androidx.test.espresso.intent.Intents
28+ import androidx.test.espresso.intent.matcher.IntentMatchers
1929import androidx.test.espresso.matcher.ViewMatchers
30+ import androidx.test.espresso.matcher.ViewMatchers.withId
31+ import androidx.test.platform.app.InstrumentationRegistry
32+ import androidx.test.uiautomator.UiSelector
2033import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
2134import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
2235import com.instructure.canvas.espresso.FeatureCategory
@@ -31,6 +44,7 @@ import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups
3144import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment
3245import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager
3346import com.instructure.canvas.espresso.mockcanvas.init
47+ import com.instructure.canvas.espresso.refresh
3448import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule
3549import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager
3650import com.instructure.canvasapi2.models.Assignment
@@ -49,6 +63,7 @@ import dagger.hilt.android.testing.UninstallModules
4963import org.hamcrest.Matchers
5064import org.junit.Assert.assertNotNull
5165import org.junit.Test
66+ import java.io.File
5267import java.util.Calendar
5368
5469@HiltAndroidTest
@@ -63,9 +78,7 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() {
6378
6479 @Test
6580 @TestMetaData(Priority .MANDATORY , FeatureCategory .SUBMISSIONS , TestCategory .INTERACTION , SecondaryFeatureCategory .SUBMISSIONS_ONLINE_URL )
66- fun testSubmission_submitAssignment () {
67- // TODO - Test submitting for each submission type
68- // For now, I'm going to just test one submission type
81+ fun testSubmission_submitOnlineURL () {
6982 val data = MockCanvas .init (
7083 studentCount = 1 ,
7184 courseCount = 1
@@ -89,6 +102,246 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() {
89102 assignmentDetailsPage.assertStatusSubmitted()
90103 }
91104
105+ @Test
106+ @TestMetaData(Priority .MANDATORY , FeatureCategory .SUBMISSIONS , TestCategory .INTERACTION , SecondaryFeatureCategory .SUBMISSIONS_TEXT_ENTRY )
107+ fun testSubmission_submitTextEntry () {
108+ val data = MockCanvas .init (
109+ studentCount = 1 ,
110+ courseCount = 1
111+ )
112+
113+ val course = data.courses.values.first()
114+ val student = data.students[0 ]
115+ val token = data.tokenFor(student)!!
116+ val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf (Assignment .SubmissionType .ONLINE_TEXT_ENTRY ))
117+ data.addSubmissionForAssignment(
118+ assignmentId = assignment.id,
119+ userId = data.users.values.first().id,
120+ type = Assignment .SubmissionType .ONLINE_TEXT_ENTRY .apiString
121+ )
122+ tokenLogin(data.domain, token, student)
123+ routeTo(" courses/${course.id} /assignments" , data.domain)
124+
125+ assignmentListPage.clickAssignment(assignment)
126+ assignmentDetailsPage.clickSubmit()
127+ textSubmissionUploadPage.typeText(" This is my test submission text." )
128+ textSubmissionUploadPage.clickOnSubmitButton()
129+ assignmentDetailsPage.assertStatusSubmitted()
130+ }
131+
132+ @Test
133+ @TestMetaData(Priority .MANDATORY , FeatureCategory .SUBMISSIONS , TestCategory .INTERACTION , SecondaryFeatureCategory .SUBMISSIONS_FILE_UPLOAD )
134+ fun testSubmission_submitFileUpload () {
135+ val data = MockCanvas .init (
136+ studentCount = 1 ,
137+ courseCount = 1
138+ )
139+
140+ val course = data.courses.values.first()
141+ val student = data.students[0 ]
142+ val token = data.tokenFor(student)!!
143+ val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf (Assignment .SubmissionType .ONLINE_UPLOAD ))
144+ data.addSubmissionForAssignment(
145+ assignmentId = assignment.id,
146+ userId = data.users.values.first().id,
147+ type = Assignment .SubmissionType .ONLINE_UPLOAD .apiString
148+ )
149+ tokenLogin(data.domain, token, student)
150+ routeTo(" courses/${course.id} /assignments" , data.domain)
151+
152+ val fileName = " test.txt"
153+ Intents .init ()
154+ try {
155+ stubFilePickerIntent(fileName)
156+ setupFileOnDevice(fileName)
157+
158+ assignmentListPage.clickAssignment(assignment)
159+ assignmentDetailsPage.clickSubmit()
160+ pickerSubmissionUploadPage.chooseDevice()
161+ pickerSubmissionUploadPage.waitForSubmitButtonToAppear()
162+ pickerSubmissionUploadPage.assertFileDisplayed(fileName)
163+ pickerSubmissionUploadPage.submit()
164+ assignmentDetailsPage.assertStatusSubmitted()
165+ } finally {
166+ Intents .release()
167+ }
168+ }
169+
170+ @Test
171+ @TestMetaData(Priority .MANDATORY , FeatureCategory .SUBMISSIONS , TestCategory .INTERACTION , SecondaryFeatureCategory .SUBMISSIONS_MEDIA_RECORDING )
172+ fun testSubmission_submitMediaRecordingChooseMediaFile () {
173+ val data = MockCanvas .init (
174+ studentCount = 1 ,
175+ courseCount = 1
176+ )
177+
178+ val course = data.courses.values.first()
179+ val student = data.students[0 ]
180+ val token = data.tokenFor(student)!!
181+ val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf (Assignment .SubmissionType .MEDIA_RECORDING ))
182+ data.addSubmissionForAssignment(
183+ assignmentId = assignment.id,
184+ userId = data.users.values.first().id,
185+ type = Assignment .SubmissionType .MEDIA_RECORDING .apiString
186+ )
187+ tokenLogin(data.domain, token, student)
188+ routeTo(" courses/${course.id} /assignments" , data.domain)
189+
190+ val activity = activityRule.activity
191+ grantPermissions(Manifest .permission.CAMERA , Manifest .permission.RECORD_AUDIO )
192+
193+ val fileName = " test_video.mp4"
194+ copyAssetFileToExternalCache(activity, fileName)
195+
196+ val resultData = Intent ()
197+ val dir = activity.externalCacheDir
198+ val file = File (dir?.path, fileName)
199+ val uri = Uri .fromFile(file)
200+ resultData.data = uri
201+ val activityResult = Instrumentation .ActivityResult (Activity .RESULT_OK , resultData)
202+
203+ Intents .init ()
204+ try {
205+ Intents .intending(
206+ Matchers .anyOf(
207+ IntentMatchers .hasAction(Intent .ACTION_GET_CONTENT ),
208+ IntentMatchers .hasAction(Intent .ACTION_PICK ),
209+ IntentMatchers .hasAction(Intent .ACTION_OPEN_DOCUMENT )
210+ )
211+ ).respondWith(activityResult)
212+
213+ assignmentListPage.clickAssignment(assignment)
214+ assignmentDetailsPage.clickSubmit()
215+
216+ onView(withId(R .id.submissionEntryMediaFile)).perform(click())
217+
218+ pickerSubmissionUploadPage.waitForSubmitButtonToAppear()
219+ pickerSubmissionUploadPage.assertFileDisplayed(fileName)
220+ pickerSubmissionUploadPage.submit()
221+ assignmentDetailsPage.assertStatusSubmitted()
222+ } finally {
223+ Intents .release()
224+ }
225+ }
226+
227+ @Test
228+ @TestMetaData(Priority .MANDATORY , FeatureCategory .SUBMISSIONS , TestCategory .INTERACTION , SecondaryFeatureCategory .SUBMISSIONS_MEDIA_RECORDING )
229+ fun testSubmission_submitMediaRecordingRecordVideo () {
230+ val data = MockCanvas .init (
231+ studentCount = 1 ,
232+ courseCount = 1
233+ )
234+
235+ val course = data.courses.values.first()
236+ val student = data.students[0 ]
237+ val token = data.tokenFor(student)!!
238+ val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf (Assignment .SubmissionType .MEDIA_RECORDING ))
239+ data.addSubmissionForAssignment(
240+ assignmentId = assignment.id,
241+ userId = data.users.values.first().id,
242+ type = Assignment .SubmissionType .MEDIA_RECORDING .apiString
243+ )
244+ tokenLogin(data.domain, token, student)
245+ routeTo(" courses/${course.id} /assignments" , data.domain)
246+
247+ val activity = activityRule.activity
248+ grantPermissions(Manifest .permission.CAMERA , Manifest .permission.RECORD_AUDIO )
249+
250+ val testVideoFile = " test_video.mp4"
251+ copyAssetFileToExternalCache(activity, testVideoFile)
252+
253+ var capturedVideoUri: Uri ? = null
254+
255+ Intents .init ()
256+ Intents .intending(
257+ Matchers .allOf(
258+ IntentMatchers .hasAction(MediaStore .ACTION_VIDEO_CAPTURE ),
259+ IntentMatchers .hasExtraWithKey(MediaStore .EXTRA_OUTPUT )
260+ )
261+ ).respondWithFunction { intent ->
262+ val outputUri = intent.extras?.get(MediaStore .EXTRA_OUTPUT ) as ? Uri
263+ capturedVideoUri = outputUri
264+ if (outputUri != null ) {
265+ val context = InstrumentationRegistry .getInstrumentation().targetContext
266+ val dir = context.externalCacheDir
267+ val sampleFile = File (dir, testVideoFile)
268+ if (outputUri.scheme == " file" ) {
269+ val destFile = File (outputUri.path!! )
270+ destFile.parentFile?.mkdirs()
271+ sampleFile.copyTo(destFile, overwrite = true )
272+ } else if (outputUri.scheme == " content" ) {
273+ context.contentResolver.openOutputStream(outputUri)?.use { outputStream ->
274+ sampleFile.inputStream().use { inputStream ->
275+ inputStream.copyTo(outputStream)
276+ }
277+ }
278+ }
279+ }
280+ Instrumentation .ActivityResult (Activity .RESULT_OK , Intent ())
281+ }
282+
283+ assignmentListPage.clickAssignment(assignment)
284+ assignmentDetailsPage.clickSubmit()
285+ onView(withId(R .id.submissionEntryVideo)).perform(click())
286+
287+ Intents .release()
288+
289+ pickerSubmissionUploadPage.waitForSubmitButtonToAppear()
290+
291+ val fileName = File (capturedVideoUri!! .path!! ).name
292+ pickerSubmissionUploadPage.assertFileDisplayed(fileName)
293+ pickerSubmissionUploadPage.submit()
294+
295+ assignmentDetailsPage.assertStatusSubmitted()
296+ }
297+
298+ @Test
299+ @TestMetaData(Priority .MANDATORY , FeatureCategory .SUBMISSIONS , TestCategory .INTERACTION , SecondaryFeatureCategory .SUBMISSIONS_MEDIA_RECORDING )
300+ fun testSubmission_submitMediaRecordingRecordAudio () {
301+ val data = MockCanvas .init (
302+ studentCount = 1 ,
303+ courseCount = 1
304+ )
305+
306+ val course = data.courses.values.first()
307+ val student = data.students[0 ]
308+ val token = data.tokenFor(student)!!
309+ val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf (Assignment .SubmissionType .MEDIA_RECORDING ))
310+ data.addSubmissionForAssignment(
311+ assignmentId = assignment.id,
312+ userId = data.users.values.first().id,
313+ type = Assignment .SubmissionType .MEDIA_RECORDING .apiString
314+ )
315+ tokenLogin(data.domain, token, student)
316+ routeTo(" courses/${course.id} /assignments" , data.domain)
317+
318+ val activity = activityRule.activity
319+ grantPermissions(Manifest .permission.RECORD_AUDIO )
320+
321+ val testAudioFileName = " test_audio.mp3"
322+ copyAssetFileToExternalCache(activity, testAudioFileName)
323+
324+ val context = InstrumentationRegistry .getInstrumentation().targetContext
325+ val recordingFile = File (context.externalCacheDir, " audio.amr" )
326+ val testAudioFile = File (context.externalCacheDir, testAudioFileName)
327+ testAudioFile.copyTo(recordingFile, overwrite = true )
328+
329+ assignmentListPage.clickAssignment(assignment)
330+ assignmentDetailsPage.clickSubmit()
331+ onView(withId(R .id.submissionEntryAudio)).perform(click())
332+
333+ device.findObject(UiSelector ().resourceIdMatches(" .*recordAudioButton" )).click()
334+
335+ testAudioFile.copyTo(recordingFile, overwrite = true )
336+
337+ device.findObject(UiSelector ().resourceIdMatches(" .*stopButton" )).click()
338+
339+ device.findObject(UiSelector ().resourceIdMatches(" .*sendAudioButton" )).click()
340+
341+ refresh()
342+ assignmentDetailsPage.assertStatusSubmitted()
343+ }
344+
92345 @Test
93346 @TestMetaData(Priority .MANDATORY , FeatureCategory .ASSIGNMENTS , TestCategory .INTERACTION )
94347 fun testSubmissionStatus_NotSubmitted () {
@@ -694,6 +947,16 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() {
694947 return assignment
695948 }
696949
950+ private fun grantPermissions (vararg permissions : String ) {
951+ val activity = activityRule.activity
952+ permissions.forEach { permission ->
953+ InstrumentationRegistry .getInstrumentation().uiAutomation.grantRuntimePermission(
954+ activity.packageName,
955+ permission
956+ )
957+ }
958+ }
959+
697960 override fun enableAndConfigureAccessibilityChecks () {
698961 extraAccessibilitySupressions = Matchers .allOf(
699962 AccessibilityCheckResultUtils .matchesCheck(
0 commit comments