Skip to content

Commit 0326544

Browse files
[MBL-19817][Student] - Implement Document Scanner interaction tests (#3587)
* Add interaction tests for document scanner. Add fake manager. Make DocumentScannerManager to be an interface + add implementation ot it and module for di. refs: MBL-19817 affects: Student release note: - * Move FakeDocumentScannerManager to espresso/mockcanvas/fakes. Add document scanner dependency to espresso build.gradle * Use assertDisplayed and assertNotDisplayed on assertion methods instead of check.
1 parent b777ed4 commit 0326544

9 files changed

Lines changed: 234 additions & 57 deletions

File tree

apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,13 @@ import com.instructure.canvas.espresso.annotations.Stub
4242
import com.instructure.canvas.espresso.mockcanvas.MockCanvas
4343
import com.instructure.canvas.espresso.mockcanvas.addAssignment
4444
import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager
45+
import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDocumentScannerManager
4546
import com.instructure.canvas.espresso.mockcanvas.init
4647
import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule
4748
import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager
4849
import com.instructure.canvasapi2.models.Assignment
50+
import com.instructure.pandautils.di.DocumentScannerModule
51+
import com.instructure.pandautils.features.file.upload.scanner.DocumentScannerManager
4952
import com.instructure.pandautils.utils.FilePrefs
5053
import com.instructure.student.ui.utils.StudentComposeTest
5154
import com.instructure.student.ui.utils.extensions.tokenLogin
@@ -61,16 +64,23 @@ import org.junit.Test
6164
import java.io.File
6265

6366
@HiltAndroidTest
64-
@UninstallModules(CustomGradeStatusModule::class)
67+
@UninstallModules(CustomGradeStatusModule::class, DocumentScannerModule::class)
6568
class PickerSubmissionUploadInteractionTest : StudentComposeTest() {
6669

6770
@BindValue
6871
@JvmField
6972
val customGradeStatusesManager: CustomGradeStatusesManager = FakeCustomGradeStatusesManager()
7073

74+
private val fakeScanner = FakeDocumentScannerManager()
75+
76+
@BindValue
77+
@JvmField
78+
val documentScannerManager: DocumentScannerManager = fakeScanner
79+
7180
override fun displaysPageObjects() = Unit
7281

7382
private val mockedFileName = "sample.jpg" // A file in our assets area
83+
private val scannerFileName = "samplepdf.pdf" // A PDF file in our assets area, because the scanner makes a PDF.
7484
private lateinit var activity : Activity
7585
private lateinit var activityResult: Instrumentation.ActivityResult
7686

@@ -82,12 +92,18 @@ class PickerSubmissionUploadInteractionTest : StudentComposeTest() {
8292
//Clear file upload cache dir.
8393
File(getInstrumentation().targetContext.cacheDir, "file_upload").deleteRecursively()
8494

85-
// Copy our sample file from the assets area to the external cache dir
95+
val dir = activity.externalCacheDir
96+
97+
// Copy our sample files from the assets area to the external cache dir
8698
copyAssetFileToExternalCache(activity, mockedFileName)
99+
copyAssetFileToExternalCache(activity, scannerFileName)
100+
101+
// Configure the scanner fake with the PDF file URI
102+
fakeScanner.scannerSupported = true
103+
fakeScanner.scanResultUri = Uri.fromFile(File(dir?.path, scannerFileName))
87104

88105
// Now create an ActivityResult that points to the sample file in the external cache dir
89106
val resultData = Intent()
90-
val dir = activity.externalCacheDir
91107
val file = File(dir?.path, mockedFileName)
92108
val uri = Uri.fromFile(file)
93109
resultData.data = uri
@@ -257,6 +273,33 @@ class PickerSubmissionUploadInteractionTest : StudentComposeTest() {
257273
// happy with that.
258274
}
259275

276+
@Test
277+
@TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION)
278+
fun testFab_scanner() {
279+
goToSubmissionPicker()
280+
pickerSubmissionUploadPage.chooseScanner()
281+
pickerSubmissionUploadPage.waitForSubmitButtonToAppear()
282+
pickerSubmissionUploadPage.assertFileDisplayed(scannerFileName)
283+
}
284+
285+
@Test
286+
@TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION)
287+
fun testFab_scannerNotAvailable() {
288+
fakeScanner.scannerSupported = false
289+
goToSubmissionPicker()
290+
pickerSubmissionUploadPage.assertScannerButtonNotDisplayed()
291+
}
292+
293+
@Test
294+
@TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION)
295+
fun testDeleteFileAfterScan() {
296+
goToSubmissionPicker()
297+
pickerSubmissionUploadPage.chooseScanner()
298+
pickerSubmissionUploadPage.waitForSubmitButtonToAppear()
299+
pickerSubmissionUploadPage.clickDeleteButton()
300+
pickerSubmissionUploadPage.assertEmptyViewDisplayed()
301+
}
302+
260303
// Seed course, user, assignment and navigate to submission picker for assignment
261304
private fun goToSubmissionPicker() : MockCanvas {
262305

apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
*/
1717
package com.instructure.student.ui.pages.classic
1818

19+
import androidx.test.espresso.Espresso.onView
1920
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
2021
import com.instructure.espresso.OnViewWithId
2122
import com.instructure.espresso.assertDisplayed
23+
import com.instructure.espresso.assertNotDisplayed
2224
import com.instructure.espresso.click
2325
import com.instructure.espresso.page.BasePage
24-
import com.instructure.espresso.page.onView
2526
import com.instructure.espresso.page.waitForViewWithText
2627
import com.instructure.espresso.page.withId
2728
import com.instructure.espresso.page.withText
@@ -33,6 +34,7 @@ class PickerSubmissionUploadPage : BasePage(R.id.pickerSubmissionUploadPage) {
3334
private val deviceIcon by OnViewWithId(R.id.sourceDeviceIcon)
3435
private val cameraIcon by OnViewWithId(R.id.sourceCameraIcon)
3536
private val galleryIcon by OnViewWithId(R.id.sourceGalleryIcon)
37+
private val scannerIcon by OnViewWithId(R.id.sourceScannerIcon)
3638
private val deleteButton by OnViewWithId(R.id.deleteButton)
3739

3840
fun chooseDevice() {
@@ -47,6 +49,18 @@ class PickerSubmissionUploadPage : BasePage(R.id.pickerSubmissionUploadPage) {
4749
galleryIcon.click()
4850
}
4951

52+
fun chooseScanner() {
53+
scannerIcon.click()
54+
}
55+
56+
fun assertScannerButtonDisplayed() {
57+
onView(withId(R.id.sourceScanner)).assertDisplayed()
58+
}
59+
60+
fun assertScannerButtonNotDisplayed() {
61+
onView(withId(R.id.sourceScanner)).assertNotDisplayed()
62+
}
63+
5064
fun waitForSubmitButtonToAppear() {
5165
waitForViewWithText(R.string.submit)
5266
}

automation/espresso/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ dependencies {
187187

188188
implementation Libs.ANDROIDX_WORK_MANAGER
189189
implementation Libs.ANDROIDX_WORK_MANAGER_KTX
190+
191+
implementation Libs.MLKIT_DOCUMENT_SCANNER
190192
}
191193

192194

automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/FileChooserPage.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class FileChooserPage : BasePage() {
4343
private val cameraButton by OnViewWithId(R.id.fromCamera)
4444
private val galleryButton by OnViewWithId(R.id.fromGallery)
4545
private val deviceButton by OnViewWithId(R.id.fromDevice)
46+
private val scannerButton by OnViewWithId(R.id.fromScanner)
4647
private val chooseFileTitle by OnViewWithId(R.id.chooseFileTitle)
4748
private val chooseFileSubtitle by OnViewWithId(R.id.chooseFileSubtitle)
4849
private val fileChooserTitle by WaitForViewWithId(R.id.alertTitle)
@@ -67,6 +68,10 @@ class FileChooserPage : BasePage() {
6768
deviceButton.scrollTo().click()
6869
}
6970

71+
fun chooseScanner() {
72+
scannerButton.scrollTo().click()
73+
}
74+
7075
fun clickOkay() {
7176
onView(withText(R.string.okay)).click()
7277
triggerWorkManagerJobs("FileUploadWorker")
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (C) 2026 - present Instructure, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.instructure.canvas.espresso.mockcanvas.fakes
17+
18+
import android.app.Activity
19+
import android.content.Intent
20+
import android.content.IntentSender
21+
import android.net.Uri
22+
import com.google.android.gms.tasks.Task
23+
import com.google.android.gms.tasks.TaskCompletionSource
24+
import com.instructure.pandautils.features.file.upload.scanner.DocumentScanResult
25+
import com.instructure.pandautils.features.file.upload.scanner.DocumentScannerManager
26+
import com.instructure.pandautils.utils.ActivityResult
27+
import com.instructure.pandautils.utils.OnActivityResults
28+
import com.instructure.pandautils.utils.postSticky
29+
30+
31+
class FakeDocumentScannerManager(
32+
private val requestCode: Int = REQUEST_DOCUMENT_SCANNING
33+
) : DocumentScannerManager {
34+
35+
var scannerSupported: Boolean = true
36+
var scanResultUri: Uri? = null
37+
38+
override fun isDeviceSupported(): Boolean = scannerSupported
39+
40+
override fun getStartScanIntent(activity: Activity, pageLimit: Int): Task<IntentSender> {
41+
val fakeResultIntent = Intent()
42+
OnActivityResults(ActivityResult(requestCode, Activity.RESULT_OK, fakeResultIntent)).postSticky()
43+
return TaskCompletionSource<IntentSender>().task
44+
}
45+
46+
override fun handleScanResultFromIntent(intent: Intent?): DocumentScanResult {
47+
return DocumentScanResult(scanResultUri, emptyList())
48+
}
49+
50+
override fun generateFileName(): String = "Scanned_Document_test.pdf"
51+
52+
companion object {
53+
const val REQUEST_DOCUMENT_SCANNING = 5103 //Matches PickerSubmissionUploadEffectHandler.REQUEST_DOCUMENT_SCANNING
54+
}
55+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (C) 2026 - present Instructure, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.instructure.pandautils.di
17+
18+
import com.instructure.pandautils.features.file.upload.scanner.DocumentScannerManager
19+
import com.instructure.pandautils.features.file.upload.scanner.DocumentScannerManagerImpl
20+
import dagger.Binds
21+
import dagger.Module
22+
import dagger.hilt.InstallIn
23+
import dagger.hilt.components.SingletonComponent
24+
import javax.inject.Singleton
25+
26+
@Module
27+
@InstallIn(SingletonComponent::class)
28+
abstract class DocumentScannerModule {
29+
30+
@Binds
31+
@Singleton
32+
abstract fun bindDocumentScannerManager(impl: DocumentScannerManagerImpl): DocumentScannerManager
33+
}

libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/scanner/DocumentScannerManager.kt

Lines changed: 6 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,64 +16,19 @@
1616
package com.instructure.pandautils.features.file.upload.scanner
1717

1818
import android.app.Activity
19-
import android.app.ActivityManager
20-
import android.content.Context
2119
import android.content.Intent
2220
import android.content.IntentSender
2321
import android.net.Uri
2422
import com.google.android.gms.tasks.Task
25-
import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions
26-
import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.RESULT_FORMAT_JPEG
27-
import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.RESULT_FORMAT_PDF
28-
import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.SCANNER_MODE_FULL
29-
import com.google.mlkit.vision.documentscanner.GmsDocumentScanning
30-
import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult
31-
import dagger.hilt.android.qualifiers.ApplicationContext
32-
import java.text.SimpleDateFormat
33-
import java.util.Date
34-
import java.util.Locale
35-
import javax.inject.Inject
36-
import javax.inject.Singleton
3723

3824
data class DocumentScanResult(
3925
val pdfUri: Uri?,
4026
val pageUris: List<Uri>
4127
)
4228

43-
@Singleton
44-
class DocumentScannerManager @Inject constructor(
45-
@ApplicationContext private val context: Context
46-
) {
47-
48-
fun isDeviceSupported(): Boolean {
49-
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
50-
val memoryInfo = ActivityManager.MemoryInfo()
51-
activityManager.getMemoryInfo(memoryInfo)
52-
val totalMemoryGB = memoryInfo.totalMem.toDouble() / (1024 * 1024 * 1024)
53-
return totalMemoryGB >= 1.7
54-
}
55-
56-
fun getStartScanIntent(activity: Activity, pageLimit: Int = 50): Task<IntentSender> {
57-
val options = GmsDocumentScannerOptions.Builder()
58-
.setScannerMode(SCANNER_MODE_FULL)
59-
.setGalleryImportAllowed(true)
60-
.setPageLimit(pageLimit)
61-
.setResultFormats(RESULT_FORMAT_PDF, RESULT_FORMAT_JPEG)
62-
.build()
63-
return GmsDocumentScanning.getClient(options).getStartScanIntent(activity)
64-
}
65-
66-
fun handleScanResultFromIntent(intent: Intent?): DocumentScanResult {
67-
val result = GmsDocumentScanningResult.fromActivityResultIntent(intent)
68-
?: return DocumentScanResult(null, emptyList())
69-
val pdfUri = result.pdf?.uri
70-
val pageUris = result.pages?.map { it.imageUri } ?: emptyList()
71-
return DocumentScanResult(pdfUri, pageUris)
72-
}
73-
74-
fun generateFileName(): String {
75-
val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
76-
val timestamp = dateFormat.format(Date())
77-
return "Scanned_Document_$timestamp.pdf"
78-
}
79-
}
29+
interface DocumentScannerManager {
30+
fun isDeviceSupported(): Boolean
31+
fun getStartScanIntent(activity: Activity, pageLimit: Int = 50): Task<IntentSender>
32+
fun handleScanResultFromIntent(intent: Intent?): DocumentScanResult
33+
fun generateFileName(): String
34+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (C) 2026 - present Instructure, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.instructure.pandautils.features.file.upload.scanner
17+
18+
import android.app.Activity
19+
import android.app.ActivityManager
20+
import android.content.Context
21+
import android.content.Intent
22+
import android.content.IntentSender
23+
import com.google.android.gms.tasks.Task
24+
import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions
25+
import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.RESULT_FORMAT_JPEG
26+
import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.RESULT_FORMAT_PDF
27+
import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.SCANNER_MODE_FULL
28+
import com.google.mlkit.vision.documentscanner.GmsDocumentScanning
29+
import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult
30+
import dagger.hilt.android.qualifiers.ApplicationContext
31+
import java.text.SimpleDateFormat
32+
import java.util.Date
33+
import java.util.Locale
34+
import javax.inject.Inject
35+
36+
class DocumentScannerManagerImpl @Inject constructor(
37+
@ApplicationContext private val context: Context
38+
) : DocumentScannerManager {
39+
40+
override fun isDeviceSupported(): Boolean {
41+
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
42+
val memoryInfo = ActivityManager.MemoryInfo()
43+
activityManager.getMemoryInfo(memoryInfo)
44+
val totalMemoryGB = memoryInfo.totalMem.toDouble() / (1024 * 1024 * 1024)
45+
return totalMemoryGB >= 1.7
46+
}
47+
48+
override fun getStartScanIntent(activity: Activity, pageLimit: Int): Task<IntentSender> {
49+
val options = GmsDocumentScannerOptions.Builder()
50+
.setScannerMode(SCANNER_MODE_FULL)
51+
.setGalleryImportAllowed(true)
52+
.setPageLimit(pageLimit)
53+
.setResultFormats(RESULT_FORMAT_PDF, RESULT_FORMAT_JPEG)
54+
.build()
55+
return GmsDocumentScanning.getClient(options).getStartScanIntent(activity)
56+
}
57+
58+
override fun handleScanResultFromIntent(intent: Intent?): DocumentScanResult {
59+
val result = GmsDocumentScanningResult.fromActivityResultIntent(intent)
60+
?: return DocumentScanResult(null, emptyList())
61+
val pdfUri = result.pdf?.uri
62+
val pageUris = result.pages?.map { it.imageUri } ?: emptyList()
63+
return DocumentScanResult(pdfUri, pageUris)
64+
}
65+
66+
override fun generateFileName(): String {
67+
val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
68+
val timestamp = dateFormat.format(Date())
69+
return "Scanned_Document_$timestamp.pdf"
70+
}
71+
}

0 commit comments

Comments
 (0)