From ce38e00b8b2f7d246f035f0c4ce5f8fe3bb69bfd Mon Sep 17 00:00:00 2001
From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com>
Date: Mon, 30 Oct 2023 12:41:24 +0100
Subject: [PATCH 01/19] [MBL-17150][Student] Offline file search (#2220)
refs: MBL-17150
affects: Student
release note: none
* Offline file search.
* File opening.
* Tests.
---
.../student/di/feature/FileSearchModule.kt | 64 ++++++++++++++
.../files/details/FileDetailsFragment.kt | 12 +--
.../features/files/list/FileListFragment.kt | 12 +--
.../files/search/FileSearchAdapter.kt | 7 +-
.../files/search/FileSearchDataSource.kt | 27 ++++++
.../files/search/FileSearchFragment.kt | 11 ++-
.../files/search/FileSearchLocalDataSource.kt | 37 ++++++++
.../search/FileSearchNetworkDataSource.kt | 34 +++++++
.../files/search/FileSearchRepository.kt | 44 ++++++++++
.../student/fragment/ParentFragment.kt | 22 ++---
.../search/FileSearchLocalDataSourceTest.kt | 88 +++++++++++++++++++
.../search/FileSearchNetworkDataSourceTest.kt | 59 +++++++++++++
.../file/search/FileSearchRepositoryTest.kt | 68 ++++++++++++++
.../canvasapi2/apis/FileFolderAPI.kt | 3 +
.../room/offline/daos/FileFolderDaoTest.kt | 29 ++++++
.../room/offline/daos/FileFolderDao.kt | 4 +
16 files changed, 479 insertions(+), 42 deletions(-)
create mode 100644 apps/student/src/main/java/com/instructure/student/di/feature/FileSearchModule.kt
create mode 100644 apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchDataSource.kt
create mode 100644 apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchLocalDataSource.kt
create mode 100644 apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchNetworkDataSource.kt
create mode 100644 apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchRepository.kt
create mode 100644 apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchLocalDataSourceTest.kt
create mode 100644 apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchNetworkDataSourceTest.kt
create mode 100644 apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchRepositoryTest.kt
diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/FileSearchModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/FileSearchModule.kt
new file mode 100644
index 0000000000..040dd05b97
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/di/feature/FileSearchModule.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 - 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.di.feature
+
+import com.instructure.canvasapi2.apis.FileFolderAPI
+import com.instructure.pandautils.room.offline.daos.FileFolderDao
+import com.instructure.pandautils.room.offline.daos.LocalFileDao
+import com.instructure.pandautils.utils.FeatureFlagProvider
+import com.instructure.pandautils.utils.NetworkStateProvider
+import com.instructure.student.features.files.search.FileSearchLocalDataSource
+import com.instructure.student.features.files.search.FileSearchNetworkDataSource
+import com.instructure.student.features.files.search.FileSearchRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+
+@Module
+@InstallIn(FragmentComponent::class)
+class FileSearchModule {
+
+ @Provides
+ fun provideFileSearchLocalDataSource(
+ fileFolderDao: FileFolderDao,
+ localFileDao: LocalFileDao
+ ): FileSearchLocalDataSource {
+ return FileSearchLocalDataSource(fileFolderDao, localFileDao)
+ }
+
+ @Provides
+ fun provideFileSearchNetworkDataSource(
+ fileFolderApi: FileFolderAPI.FilesFoldersInterface
+ ): FileSearchNetworkDataSource {
+ return FileSearchNetworkDataSource(fileFolderApi)
+ }
+
+ @Provides
+ fun provideFileSearchRepository(
+ fileSearchLocalDataSource: FileSearchLocalDataSource,
+ fileSearchNetworkDataSource: FileSearchNetworkDataSource,
+ networkStateProvider: NetworkStateProvider,
+ featureFlagProvider: FeatureFlagProvider
+ ): FileSearchRepository {
+ return FileSearchRepository(
+ fileSearchLocalDataSource,
+ fileSearchNetworkDataSource,
+ networkStateProvider,
+ featureFlagProvider)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt
index 6908031cde..4c4bfbbfe4 100644
--- a/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt
@@ -119,17 +119,7 @@ class FileDetailsFragment : ParentFragment() {
private fun setupClickListeners() {
binding.openButton.setOnClickListener {
file?.let { fileFolder ->
- when {
- fileFolder.isLocalFile -> {
- openLocalMedia(
- fileFolder.contentType,
- fileFolder.url,
- fileFolder.displayName,
- canvasContext
- )
- }
- else -> openMedia(fileFolder.contentType, fileFolder.url, fileFolder.displayName, canvasContext)
- }
+ openMedia(fileFolder.contentType, fileFolder.url, fileFolder.displayName, canvasContext, fileFolder.isLocalFile)
markAsRead()
}
}
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 ace8cd4d9f..553bb47c90 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
@@ -222,13 +222,9 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent
recordFilePreviewEvent(item)
openHtmlUrl(item)
}
- item.isLocalFile -> {
- recordFilePreviewEvent(item)
- openLocalMedia(item.contentType, item.url, item.displayName, canvasContext)
- }
else -> {
recordFilePreviewEvent(item)
- openMedia(item.contentType, item.url, item.displayName, canvasContext)
+ openMedia(item.contentType, item.url, item.displayName, canvasContext, localFile = item.isLocalFile)
}
}
}
@@ -378,11 +374,7 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent
when (menuItem.itemId) {
R.id.openAlternate -> {
recordFilePreviewEvent(item)
- if (fileListRepository.isOnline()) {
- openMedia(item.contentType, item.url, item.displayName, true, canvasContext)
- } else {
- openLocalMedia(item.contentType, item.url, item.displayName, canvasContext, true)
- }
+ openMedia(item.contentType, item.url, item.displayName, canvasContext, localFile = !fileListRepository.isOnline(), useOutsideApps = true)
}
R.id.download -> downloadItem(item)
R.id.rename -> renameItem(item)
diff --git a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchAdapter.kt
index d0b4a54768..35cc826a59 100644
--- a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchAdapter.kt
@@ -26,6 +26,8 @@ import com.instructure.canvasapi2.utils.weave.WeaveJob
import com.instructure.canvasapi2.utils.weave.awaitApi
import com.instructure.canvasapi2.utils.weave.catch
import com.instructure.canvasapi2.utils.weave.tryWeave
+import com.instructure.pandautils.room.offline.daos.FileFolderDao
+import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.pandautils.utils.textAndIconColor
import com.instructure.student.adapter.BaseListRecyclerAdapter
import com.instructure.student.features.files.list.FileFolderCallback
@@ -34,6 +36,7 @@ import com.instructure.student.holders.FileViewHolder
class FileSearchAdapter(
context: Context,
private val canvasContext: CanvasContext,
+ private val fileSearchRepository: FileSearchRepository,
private val viewCallback: FileSearchView
) : BaseListRecyclerAdapter(context, FileFolder::class.java) {
@@ -83,9 +86,7 @@ class FileSearchAdapter(
private fun performSearch() {
apiCall = tryWeave {
viewCallback.onRefreshStarted()
- val files = awaitApi> {
- FileFolderManager.searchFiles(searchQuery, canvasContext, true, it)
- }
+ val files = fileSearchRepository.searchFiles(canvasContext, searchQuery)
clear()
addAll(files)
viewCallback.onRefreshFinished()
diff --git a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchDataSource.kt
new file mode 100644
index 0000000000..263fba8b2c
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchDataSource.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 - 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.features.files.search
+
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.FileFolder
+
+interface FileSearchDataSource {
+
+ suspend fun searchFiles(canvasContext: CanvasContext, searchQuery: String): List
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchFragment.kt b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchFragment.kt
index 39342f2b74..3cf60d7cf3 100644
--- a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchFragment.kt
@@ -31,24 +31,31 @@ import com.instructure.interactions.router.Route
import com.instructure.pandautils.analytics.SCREEN_VIEW_FILE_SEARCH
import com.instructure.pandautils.analytics.ScreenView
import com.instructure.pandautils.binding.viewBinding
+import com.instructure.pandautils.room.offline.daos.FileFolderDao
import com.instructure.pandautils.utils.*
import com.instructure.student.R
import com.instructure.student.databinding.FragmentFileSearchBinding
import com.instructure.student.fragment.ParentFragment
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
import com.instructure.pandautils.utils.ColorUtils as PandaColorUtils
@ScreenView(SCREEN_VIEW_FILE_SEARCH)
+@AndroidEntryPoint
class FileSearchFragment : ParentFragment(), FileSearchView {
private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT)
private val binding by viewBinding(FragmentFileSearchBinding::bind)
+ @Inject
+ lateinit var fileSearchRepository: FileSearchRepository
+
private fun makePageViewUrl() =
if (canvasContext.type == CanvasContext.Type.USER) "${ApiPrefs.fullDomain}/files"
else "${ApiPrefs.fullDomain}/${canvasContext.contextId.replace("_", "s/")}/files"
- private val searchAdapter by lazy { FileSearchAdapter(requireContext(), canvasContext, this) }
+ private val searchAdapter by lazy { FileSearchAdapter(requireContext(), canvasContext, fileSearchRepository, this) }
override fun title() = ""
override fun applyTheme() = Unit
@@ -137,7 +144,7 @@ class FileSearchFragment : ParentFragment(), FileSearchView {
override fun fileClicked(file: FileFolder) {
PageViewUtils.saveSingleEvent("FilePreview", "${makePageViewUrl()}?preview=${file.id}")
- openMedia(file.contentType, file.url, file.displayName, canvasContext)
+ openMedia(file.contentType, file.url, file.displayName, canvasContext, file.isLocalFile)
}
override fun onMediaLoadingStarted() {
diff --git a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchLocalDataSource.kt
new file mode 100644
index 0000000000..cb054d231e
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchLocalDataSource.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 - 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.features.files.search
+
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.FileFolder
+import com.instructure.pandautils.room.offline.daos.FileFolderDao
+import com.instructure.pandautils.room.offline.daos.LocalFileDao
+
+class FileSearchLocalDataSource(
+ private val fileFolderDao: FileFolderDao,
+ private val localFileDao: LocalFileDao
+) : FileSearchDataSource {
+ override suspend fun searchFiles(canvasContext: CanvasContext, searchQuery: String): List {
+ val files = fileFolderDao.searchCourseFiles(canvasContext.id, searchQuery).map { it.toApiModel() }
+ val fileIds = files.map { it.id }
+ val localFileMap = localFileDao.findByIds(fileIds).associate { it.id to it.path }
+
+ return files.map { it.copy(url = localFileMap[it.id], thumbnailUrl = null) }
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchNetworkDataSource.kt
new file mode 100644
index 0000000000..2f8a42cb24
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchNetworkDataSource.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 - 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.features.files.search
+
+import com.instructure.canvasapi2.apis.FileFolderAPI
+import com.instructure.canvasapi2.builders.RestParams
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.FileFolder
+import com.instructure.canvasapi2.utils.depaginate
+
+class FileSearchNetworkDataSource(private val fileFolderApi: FileFolderAPI.FilesFoldersInterface) : FileSearchDataSource {
+ override suspend fun searchFiles(canvasContext: CanvasContext, searchQuery: String): List {
+ val params = RestParams(isForceReadFromNetwork = true, usePerPageQueryParam = true)
+ return fileFolderApi.searchFiles(canvasContext.toAPIString().substring(1), searchQuery, params)
+ .depaginate { nextUrl -> fileFolderApi.getNextPageFileFoldersList(nextUrl, params) }
+ .dataOrNull.orEmpty()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchRepository.kt b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchRepository.kt
new file mode 100644
index 0000000000..b0a1fec013
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchRepository.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 - 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.features.files.search
+
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.FileFolder
+import com.instructure.pandautils.repository.Repository
+import com.instructure.pandautils.utils.FeatureFlagProvider
+import com.instructure.pandautils.utils.NetworkStateProvider
+
+class FileSearchRepository(
+ fileSearchLocalDataSource: FileSearchLocalDataSource,
+ fileSearchNetworkDataSource: FileSearchNetworkDataSource,
+ networkStateProvider: NetworkStateProvider,
+ featureFlagProvider: FeatureFlagProvider
+) : Repository(
+ fileSearchLocalDataSource,
+ fileSearchNetworkDataSource,
+ networkStateProvider,
+ featureFlagProvider
+) {
+ suspend fun searchFiles(
+ canvasContext: CanvasContext,
+ searchQuery: String
+ ): List {
+ return dataSource().searchFiles(canvasContext, searchQuery)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt
index 2f52aadbb7..4cbdbeb85e 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt
@@ -371,18 +371,16 @@ abstract class ParentFragment : DialogFragment(), FragmentInteractions, Navigati
return recyclerView
}
- fun openMedia(mime: String?, url: String?, filename: String?, canvasContext: CanvasContext) {
+ fun openMedia(mime: String?, url: String?, filename: String?, canvasContext: CanvasContext, localFile: Boolean = false, useOutsideApps: Boolean = false) {
val owner = activity ?: return
- onMainThread {
- openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(canvasContext, mime, url, filename)
- LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(owner), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID)
+
+ openMediaBundle = if (localFile) {
+ OpenMediaAsyncTaskLoader.createLocalBundle(canvasContext, mime, url, filename, useOutsideApps)
+ } else {
+ OpenMediaAsyncTaskLoader.createBundle(canvasContext, mime, url, filename, useOutsideApps)
}
- }
- fun openLocalMedia(mime: String?, path: String?, filename: String?, canvasContext: CanvasContext, useOutsideApps: Boolean = false) {
- val owner = activity ?: return
onMainThread {
- openMediaBundle = OpenMediaAsyncTaskLoader.createLocalBundle(canvasContext, mime, path, filename, useOutsideApps)
LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(owner), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID)
}
}
@@ -395,14 +393,6 @@ abstract class ParentFragment : DialogFragment(), FragmentInteractions, Navigati
}
}
- fun openMedia(mime: String?, url: String?, filename: String?, useOutsideApps: Boolean, canvasContext: CanvasContext) {
- val owner = activity ?: return
- onMainThread {
- openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(canvasContext, mime, url, filename, useOutsideApps)
- LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(owner), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID)
- }
- }
-
private fun downloadFileToDownloadDir(url: String): File? {
// We should have the file cached locally at this point; We'll just move it to the user's Downloads folder
diff --git a/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchLocalDataSourceTest.kt
new file mode 100644
index 0000000000..c9a46e177a
--- /dev/null
+++ b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchLocalDataSourceTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 - 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.file.search
+
+import android.webkit.URLUtil
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.FileFolder
+import com.instructure.pandautils.room.offline.daos.FileFolderDao
+import com.instructure.pandautils.room.offline.daos.LocalFileDao
+import com.instructure.pandautils.room.offline.entities.FileFolderEntity
+import com.instructure.pandautils.room.offline.entities.LocalFileEntity
+import com.instructure.student.features.files.search.FileSearchLocalDataSource
+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
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import java.util.Date
+
+@ExperimentalCoroutinesApi
+class FileSearchLocalDataSourceTest {
+
+ private val fileFolderDao: FileFolderDao = mockk(relaxed = true)
+ private val localFileDao: LocalFileDao = mockk(relaxed = true)
+
+ private val fileSearchLocalDataSource = FileSearchLocalDataSource(fileFolderDao, localFileDao)
+
+ @Before
+ fun setup() {
+ mockkStatic(URLUtil::class)
+ every { URLUtil.isNetworkUrl(any()) } returns true
+ }
+
+ @After
+ fun teardown() {
+ unmockkAll()
+ }
+
+
+ @Test
+ fun `File Search replaces url with path`() = runTest {
+ val files = listOf(
+ FileFolder(id = 1, name = "File 1", url = "url_1", thumbnailUrl = "thumbnail_url_1"),
+ FileFolder(id = 2, name = "File 2", url = "url_2", thumbnailUrl = "thumbnail_url_2"),
+ FileFolder(id = 3, name = "File 3", url = "url_3", thumbnailUrl = "thumbnail_url_3")
+ )
+
+ val localFiles = listOf(
+ LocalFileEntity(id = 1, courseId = 1, createdDate = Date(), path = "path_1"),
+ LocalFileEntity(id = 2, courseId = 1, createdDate = Date(), path = "path_2"),
+ LocalFileEntity(id = 3, courseId = 1, createdDate = Date(), path = "path_3")
+ )
+
+ coEvery { localFileDao.findByIds(any()) } returns localFiles
+ coEvery { fileFolderDao.searchCourseFiles(any(), any()) } returns files.map { FileFolderEntity(it) }
+
+ val expected = files.map { it.copy(url = "path_${it.id}", thumbnailUrl = null) }
+ val result = fileSearchLocalDataSource.searchFiles(Course(1L), "")
+
+ coVerify {
+ localFileDao.findByIds(listOf(1, 2, 3))
+ fileFolderDao.searchCourseFiles(1L, "")
+ }
+
+ TestCase.assertEquals(expected, result)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchNetworkDataSourceTest.kt
new file mode 100644
index 0000000000..a8e68b0870
--- /dev/null
+++ b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchNetworkDataSourceTest.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 - 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.file.search
+
+import com.instructure.canvasapi2.apis.FileFolderAPI
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.FileFolder
+import com.instructure.canvasapi2.utils.DataResult
+import com.instructure.student.features.files.search.FileSearchNetworkDataSource
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class FileSearchNetworkDataSourceTest {
+
+ private val fileFolderApi: FileFolderAPI.FilesFoldersInterface = mockk(relaxed = true)
+
+ private val fileSearchNetworkDataSource = FileSearchNetworkDataSource(fileFolderApi)
+
+ @Test
+ fun `searchFiles() calls api and returns data from api`() = runTest {
+ coEvery { fileFolderApi.searchFiles(any(), any(), any()) } returns DataResult.Success(listOf(FileFolder(id = 1, name = "File")))
+
+ val result = fileSearchNetworkDataSource.searchFiles(Course(1), "file")
+
+ coVerify { fileFolderApi.searchFiles("courses/1", "file",any()) }
+ assertEquals(1, result.size)
+ assertEquals("File", result[0].name)
+ }
+
+ @Test
+ fun `searchFiles() returns empty list for failed result`() = runTest {
+ coEvery { fileFolderApi.searchFiles(any(), any(), any()) } returns DataResult.Fail()
+
+ val result = fileSearchNetworkDataSource.searchFiles(Course(1), "file")
+
+ coVerify { fileFolderApi.searchFiles("courses/1", "file",any()) }
+ assertEquals(0, result.size)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchRepositoryTest.kt
new file mode 100644
index 0000000000..a6bed1cf59
--- /dev/null
+++ b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchRepositoryTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 - 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.features.file.search
+
+import com.instructure.pandautils.utils.FeatureFlagProvider
+import com.instructure.pandautils.utils.NetworkStateProvider
+import com.instructure.student.features.files.search.FileSearchLocalDataSource
+import com.instructure.student.features.files.search.FileSearchNetworkDataSource
+import com.instructure.student.features.files.search.FileSearchRepository
+import io.mockk.coEvery
+import io.mockk.mockk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class FileSearchRepositoryTest {
+
+ private val fileSearchLocalDataSource: FileSearchLocalDataSource = mockk(relaxed = true)
+ private val fileSearchNetworkDataSource: FileSearchNetworkDataSource = mockk(relaxed = true)
+ private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true)
+ private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true)
+
+ private val fileSearchRepository = FileSearchRepository(
+ fileSearchLocalDataSource,
+ fileSearchNetworkDataSource,
+ networkStateProvider,
+ featureFlagProvider
+ )
+
+ @Before
+ fun setup() {
+ coEvery { featureFlagProvider.offlineEnabled() } returns true
+ coEvery { networkStateProvider.isOnline() } returns true
+ }
+
+ @Test
+ fun `use localDataSource when network is offline`() = runTest {
+ coEvery { networkStateProvider.isOnline() } returns false
+
+ assertTrue(fileSearchRepository.dataSource() is FileSearchLocalDataSource)
+ }
+
+ @Test
+ fun `use networkDataSource when network is online`() = runTest {
+ coEvery { networkStateProvider.isOnline() } returns true
+
+ assertTrue(fileSearchRepository.dataSource() is FileSearchNetworkDataSource)
+ }
+}
\ No newline at end of file
diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt
index 511e8c4759..0f8beec278 100644
--- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt
+++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt
@@ -89,6 +89,9 @@ object FileFolderAPI {
@GET("{canvasContext}/files")
fun searchFiles(@Path(value = "canvasContext", encoded = true) contextPath: String, @Query("search_term") query: String): Call>
+ @GET("{canvasContext}/files")
+ suspend fun searchFiles(@Path(value = "canvasContext", encoded = true) contextPath: String, @Query("search_term") query: String, @Tag params: RestParams): DataResult>
+
@DELETE("files/{fileId}")
fun deleteFile(@Path("fileId") fileId: Long): Call
diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileFolderDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileFolderDaoTest.kt
index be60845c7a..b412ce5b54 100644
--- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileFolderDaoTest.kt
+++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileFolderDaoTest.kt
@@ -574,4 +574,33 @@ class FileFolderDaoTest {
assertEquals(listOf(files[2]), result)
}
+
+ @Test
+ fun testSearchFiles() = runTest {
+ val folders = listOf(
+ FileFolderEntity(
+ FileFolder(
+ id = 1L,
+ contextId = 1L,
+ contextType = "Course",
+ name = "folder",
+ parentFolderId = 0
+ )
+ )
+ )
+ val files = listOf(
+ FileFolderEntity(FileFolder(id = 2L, displayName = "file1", folderId = 1L)),
+ FileFolderEntity(FileFolder(id = 3L, displayName = "file2", folderId = 1L)),
+ FileFolderEntity(FileFolder(id = 4L, displayName = "different name", folderId = 1L)),
+ FileFolderEntity(FileFolder(id = 5L, displayName = "file hidden", folderId = 1L, isHidden = true)),
+ FileFolderEntity(FileFolder(id = 6L, displayName = "file hidden for user", folderId = 1L, isHiddenForUser = true)),
+ )
+
+ fileFolderDao.insertAll(folders)
+ fileFolderDao.insertAll(files)
+
+ val result = fileFolderDao.searchCourseFiles(1L, "fil")
+
+ assertEquals(files.subList(0, 2), result)
+ }
}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileFolderDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileFolderDao.kt
index 8950d6ef61..55afa8385d 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileFolderDao.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileFolderDao.kt
@@ -44,6 +44,10 @@ abstract class FileFolderDao {
@Query("SELECT * FROM FileFolderEntity WHERE folderId in (SELECT id FROM FileFolderEntity WHERE contextId = :courseId)")
abstract suspend fun findAllFilesByCourseId(courseId: Long): List
+ @Query("SELECT * FROM FileFolderEntity WHERE folderId in (SELECT id FROM FileFolderEntity WHERE contextId = :courseId)" +
+ " AND displayName LIKE '%' || :searchQuery || '%' AND isHidden = 0 AND isHiddenForUser = 0")
+ abstract suspend fun searchCourseFiles(courseId: Long, searchQuery: String): List
+
@Query("SELECT * FROM FileFolderEntity WHERE id = :id")
abstract suspend fun findById(id: Long): FileFolderEntity?
From 55a2af5941d6ca2c6adbaf10e28caf54997ee5d5 Mon Sep 17 00:00:00 2001
From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com>
Date: Thu, 2 Nov 2023 12:03:12 +0100
Subject: [PATCH 02/19] [MBL-17117][Student] - Manage Offline Content Page E2E
test implementation (#2223)
---
.../ui/e2e/offline/DashboardE2EOfflineTest.kt | 2 +-
.../offline/ManageOfflineContentE2ETest.kt | 218 ++++++++++++++++++
.../e2e/offline/OfflineSyncProgressE2ETest.kt | 2 +-
.../student/ui/pages/CourseGradesPage.kt | 4 +
.../student/ui/pages/DashboardPage.kt | 3 +-
.../ui/pages/LeftSideNavigationDrawerPage.kt | 1 +
.../pages/offline/ManageOfflineContentPage.kt | 88 ++++++-
.../canvas/espresso/CustomMatchers.kt | 25 ++
.../espresso/CustomViewAssertions.kt | 28 ++-
.../espresso/actions/ForceClick.kt | 50 ++++
.../panda_annotations/TestMetaData.kt | 2 +-
11 files changed, 414 insertions(+), 9 deletions(-)
create mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt
create mode 100644 automation/espresso/src/main/kotlin/com/instructure/espresso/actions/ForceClick.kt
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt
index d40df12d12..c8e1e8bcae 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt
@@ -55,7 +55,7 @@ class DashboardE2EOfflineTest : StudentTest() {
dashboardPage.openGlobalManageOfflineContentPage()
Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.")
- manageOfflineContentPage.selectEntireCourseForSync(course1.name)
+ manageOfflineContentPage.changeItemSelectionState(course1.name)
manageOfflineContentPage.clickOnSyncButton()
Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.")
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/offline/ManageOfflineContentE2ETest.kt
new file mode 100644
index 0000000000..acbc812997
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2023 - 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.e2e.offline
+
+import android.util.Log
+import androidx.test.espresso.Espresso
+import com.google.android.material.checkbox.MaterialCheckBox
+import com.instructure.canvas.espresso.OfflineE2E
+import com.instructure.panda_annotations.FeatureCategory
+import com.instructure.panda_annotations.Priority
+import com.instructure.panda_annotations.TestCategory
+import com.instructure.panda_annotations.TestMetaData
+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 dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.After
+import org.junit.Test
+
+@HiltAndroidTest
+class ManageOfflineContentE2ETest : StudentTest() {
+ override fun displaysPageObjects() = Unit
+
+ override fun enableAndConfigureAccessibilityChecks() = Unit
+
+ @OfflineE2E
+ @Test
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.OFFLINE_CONTENT, TestCategory.E2E)
+ fun testManageOfflineContentE2ETest() {
+
+ Log.d(PREPARATION_TAG,"Seeding data.")
+ val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1)
+ val student = data.studentsList[0]
+ val course1 = data.coursesList[0]
+ val course2 = data.coursesList[1]
+ val testAnnouncement = data.announcementsList[0]
+
+ Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
+ tokenLogin(student)
+ dashboardPage.waitForRender()
+
+ Log.d(STEP_TAG, "Open global 'Manage Offline Content' page via the more menu of the Dashboard Page.")
+ dashboardPage.clickCourseOverflowMenu(course1.name, "Manage Offline Content")
+
+ Log.d(STEP_TAG, "Assert that if there is nothing selected yet, the 'SELECT ALL' button text will be displayed on the top-left corner of the toolbar.")
+ manageOfflineContentPage.assertSelectButtonText(selectAll = true)
+
+ Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state is 'Unchecked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_UNCHECKED)
+
+ Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync.")
+ manageOfflineContentPage.changeItemSelectionState(course1.name)
+
+ Log.d(STEP_TAG, "Assert that if there is something selected yet, the 'DESELECT ALL' button text will be displayed on the top-left corner of the toolbar.")
+ manageOfflineContentPage.assertSelectButtonText(selectAll = false)
+
+ Log.d(STEP_TAG, "Assert that the Storage info details are displayed properly.")
+ manageOfflineContentPage.assertStorageInfoDetails()
+
+ Log.d(STEP_TAG, "Assert that the tool bar texts are displayed properly, so the subtitle is '${course1.name}', because we are on the Manage Offline Content page of '${course1.name}' course.")
+ manageOfflineContentPage.assertToolbarTexts(course1.name)
+
+ Log.d(STEP_TAG, "Deselect the 'Announcements' and 'Discussions' of the '${course1.name}' course.")
+ manageOfflineContentPage.changeItemSelectionState("Announcements")
+ manageOfflineContentPage.changeItemSelectionState("Discussions")
+
+ Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state is 'Indeterminate'.")
+ manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_INDETERMINATE)
+
+ Log.d(STEP_TAG, "Select the entire '${course1.name}' course (again) for sync.")
+ manageOfflineContentPage.changeItemSelectionState(course1.name)
+
+ Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state and became 'Checked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_CHECKED)
+
+ Log.d(STEP_TAG, "Assert that the previously unchecked 'Announcements' and 'Discussions' checkboxes are became 'Checked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem("Announcements", MaterialCheckBox.STATE_CHECKED)
+ manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_CHECKED)
+
+ Log.d(STEP_TAG, "Assert that the 'DESELECT ALL' button is still displayed because there are still more than zero item checked.")
+ manageOfflineContentPage.assertSelectButtonText(selectAll = false)
+
+ Log.d(STEP_TAG, "Deselect '${course1.name}' course's checkbox.")
+ manageOfflineContentPage.changeItemSelectionState(course1.name)
+
+ Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state and became 'Unchecked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_UNCHECKED)
+
+ Log.d(STEP_TAG, "Assert that the previously unchecked 'Announcements' and 'Discussions' checkboxes are became 'Unchecked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem("Announcements", MaterialCheckBox.STATE_UNCHECKED)
+ manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_UNCHECKED)
+
+ Log.d(STEP_TAG, "Assert that the 'SELECT ALL' button is displayed because there is no item checked.")
+ manageOfflineContentPage.assertSelectButtonText(selectAll = true)
+
+ Log.d(STEP_TAG, "Click on 'SELECT ALL' button.")
+ manageOfflineContentPage.clickOnSelectAllButton()
+
+ Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state and became 'Checked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_CHECKED)
+
+ Log.d(STEP_TAG, "Assert that the previously unchecked 'Announcements' and 'Discussions' checkboxes are became 'Checked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem("Announcements", MaterialCheckBox.STATE_CHECKED)
+ manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_CHECKED)
+
+ Log.d(STEP_TAG, "Assert that the 'DESELECT ALL' will be displayed after clicking the 'SELECT ALL' button.")
+ manageOfflineContentPage.assertSelectButtonText(selectAll = false)
+
+ Log.d(STEP_TAG, "Click on 'DESELECT ALL' button.")
+ manageOfflineContentPage.clickOnDeselectAllButton()
+
+ Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state that it became 'Unchecked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_UNCHECKED)
+
+ Log.d(STEP_TAG, "Assert that the previously checked 'Announcements' and 'Discussions' checkboxes are became 'Unchecked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem("Announcements", MaterialCheckBox.STATE_UNCHECKED)
+ manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_UNCHECKED)
+
+ Log.d(STEP_TAG, "Assert that the 'SELECT ALL' will be displayed after clicking the 'SELECT ALL' button.")
+ manageOfflineContentPage.assertSelectButtonText(selectAll = true)
+
+ Log.d(STEP_TAG, "Navigate back to Dashboard Page. Open 'Global' Manage Offline Content page.")
+ Espresso.pressBack()
+ dashboardPage.openGlobalManageOfflineContentPage()
+
+ Log.d(STEP_TAG, "Assert that the Storage info details are displayed properly.")
+ manageOfflineContentPage.assertStorageInfoDetails()
+
+ Log.d(STEP_TAG, "Assert that the tool bar texts are displayed properly, so the subtitle is 'All Courses', because we are on the 'Global' Manage Offline Content page.")
+ manageOfflineContentPage.assertToolbarTexts("All Courses")
+
+ Log.d(STEP_TAG, "Assert that the '${course1.name}' and '${course2.name}' courses' checkboxes are 'Unchecked' yet.")
+ manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_UNCHECKED)
+ manageOfflineContentPage.assertCheckedStateOfItem(course2.name, MaterialCheckBox.STATE_UNCHECKED)
+
+ Log.d(STEP_TAG, "Expand '${course1.name}' course.")
+ manageOfflineContentPage.expandCollapseItem(course1.name)
+
+ Log.d(STEP_TAG, "Assert that the 'Announcements' and 'Discussions' items are 'unchecked' (and so all the other tabs of the course) because the course is NOT selected.")
+ manageOfflineContentPage.assertCheckedStateOfItem("Announcements", MaterialCheckBox.STATE_UNCHECKED)
+ manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_UNCHECKED)
+
+ Log.d(STEP_TAG, "Collapse '${course1.name}' course.")
+ manageOfflineContentPage.expandCollapseItem(course1.name)
+
+ manageOfflineContentPage.waitForItemDisappear("Announcements")
+ manageOfflineContentPage.waitForItemDisappear("Discussions")
+
+ Thread.sleep(1000) //need to wait 1 second here because sometimes expand/collapse happens too fast
+ Log.d(STEP_TAG, "Expand '${course2.name}' course.")
+ manageOfflineContentPage.expandCollapseItem(course2.name)
+
+ Log.d(STEP_TAG, "Assert that the 'Grades' and 'Discussions' items are 'Unchecked' (and so all the other tabs of the course) because the course has not selected.")
+ manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_UNCHECKED)
+ manageOfflineContentPage.assertCheckedStateOfItem("Grades", MaterialCheckBox.STATE_UNCHECKED)
+
+ Log.d(STEP_TAG, "Check '${course2.name}' course.")
+ manageOfflineContentPage.changeItemSelectionState(course2.name)
+
+ Log.d(STEP_TAG, "Assert that the '${course2.name}' course's checkbox state and became 'Checked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem(course2.name, MaterialCheckBox.STATE_CHECKED)
+
+ Log.d(STEP_TAG, "Assert that the 'Grades' and 'Discussions' items are 'checked' (and so all the other tabs of the course) because the course has selected.")
+ manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_CHECKED)
+ manageOfflineContentPage.assertCheckedStateOfItem("Grades", MaterialCheckBox.STATE_CHECKED)
+
+ Log.d(STEP_TAG, "Collapse '${course2.name}' course.")
+ manageOfflineContentPage.expandCollapseItem(course2.name)
+
+ Log.d(STEP_TAG, "Assert that both of the seeded courses are displayed as a selectable item in the Manage Offline Content page.")
+ manageOfflineContentPage.assertCourseCountWithMatcher(2)
+
+ Log.d(STEP_TAG, "Click on the 'Sync' button.")
+ manageOfflineContentPage.clickOnSyncButton()
+
+ Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.")
+ dashboardPage.waitForRender()
+ dashboardPage.waitForSyncProgressDownloadStartedNotification()
+ dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear()
+
+ Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)")
+ dashboardPage.waitForSyncProgressStartingNotification()
+ dashboardPage.waitForSyncProgressStartingNotificationToDisappear()
+
+ Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.")
+ OfflineTestUtils.turnOffConnectionViaADB()
+ dashboardPage.waitForRender()
+
+ Log.d(STEP_TAG, "Select '${course2.name}' course and open 'Grades' menu to check if it's really synced and can be seen in offline mode.")
+ dashboardPage.selectCourse(course2)
+ courseBrowserPage.selectGrades()
+
+ Log.d(STEP_TAG,"Assert that the empty view is displayed on the 'Grades' page (just to check that it's available in offline mode.")
+ courseGradesPage.assertEmptyView()
+ }
+
+ @After
+ fun tearDown() {
+ Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, so it will come back online.")
+ OfflineTestUtils.turnOnConnectionViaADB()
+ }
+
+}
\ No newline at end of file
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/offline/OfflineSyncProgressE2ETest.kt
index 7c3413a09f..228df34db0 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/offline/OfflineSyncProgressE2ETest.kt
@@ -57,7 +57,7 @@ class OfflineSyncProgressE2ETest : StudentTest() {
dashboardPage.openGlobalManageOfflineContentPage()
Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.")
- manageOfflineContentPage.selectEntireCourseForSync(course1.name)
+ manageOfflineContentPage.changeItemSelectionState(course1.name)
manageOfflineContentPage.clickOnSyncButton()
Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.")
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/CourseGradesPage.kt
index 58f45c6131..a7f4775120 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/CourseGradesPage.kt
@@ -82,6 +82,10 @@ class CourseGradesPage : BasePage(R.id.courseGradesPage) {
gradeValue.check(matches(matcher))
}
+ fun assertEmptyView() {
+ onView(withId(R.id.title) + withText(R.string.noItemsToDisplayShort) + withAncestor(R.id.gradesEmptyView)).assertDisplayed()
+ }
+
fun assertAssignmentDisplayed(name: String, gradeString: String) {
val siblingMatcher = withId(R.id.title) + withParent(R.id.textContainer) + withText(name) + withAncestor(R.id.courseGradesPage)
onView(withId(R.id.points) + hasSibling(siblingMatcher)).scrollTo().assertHasText(gradeString)
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/DashboardPage.kt
index 116ea38c00..78121a4c7e 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/DashboardPage.kt
@@ -260,6 +260,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
.perform(click());
}
+ //OfflineMethod
fun openGlobalManageOfflineContentPage() {
Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext)
onView(withText(containsString("Manage Offline Content")))
@@ -307,7 +308,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
withId(R.id.cardView)
+ withDescendant(withId(R.id.titleTextView) + withText(courseTitle))
)
- onView(courseOverflowMatcher).scrollTo().click()
+ waitForView(courseOverflowMatcher).scrollTo().click()
waitForView(withId(R.id.title) + withText(menuTitle)).click()
}
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/LeftSideNavigationDrawerPage.kt
index dd263c60e8..bb0300d219 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/LeftSideNavigationDrawerPage.kt
@@ -65,6 +65,7 @@ class LeftSideNavigationDrawerPage : BasePage() {
)
private fun clickMenu(menuId: Int) {
+ sleep(1000) //to avoid listview a11y error (content description is missing)
waitForView(hamburgerButtonMatcher).click()
waitForViewWithId(menuId).scrollTo().click()
}
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/offline/ManageOfflineContentPage.kt
index 96c5e6ffea..c7e6efd27e 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/offline/ManageOfflineContentPage.kt
@@ -17,8 +17,18 @@
package com.instructure.student.ui.pages.offline
+import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import com.instructure.canvas.espresso.containsTextCaseInsensitive
+import com.instructure.canvas.espresso.hasCheckedState
+import com.instructure.espresso.ConstraintLayoutItemCountAssertion
+import com.instructure.espresso.ConstraintLayoutItemCountAssertionWithMatcher
+import com.instructure.espresso.DoesNotExistAssertion
import com.instructure.espresso.OnViewWithId
+import com.instructure.espresso.WaitForViewWithId
+import com.instructure.espresso.actions.ForceClick
+import com.instructure.espresso.assertDisplayed
import com.instructure.espresso.click
import com.instructure.espresso.page.BasePage
import com.instructure.espresso.page.onView
@@ -26,21 +36,35 @@ import com.instructure.espresso.page.plus
import com.instructure.espresso.page.waitForView
import com.instructure.espresso.page.withAncestor
import com.instructure.espresso.page.withId
+import com.instructure.espresso.page.withParent
import com.instructure.espresso.page.withText
+import com.instructure.espresso.scrollTo
import com.instructure.pandautils.R
+import org.hamcrest.CoreMatchers.allOf
class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) {
- private val toolbar by OnViewWithId(R.id.toolbar)
+ private val syncButton by OnViewWithId(R.id.syncButton)
+ private val storageInfoContainer by WaitForViewWithId(R.id.storageInfoContainer)
//OfflineMethod
- fun selectEntireCourseForSync(courseName: String) {
- onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(courseName))).click()
+ fun changeItemSelectionState(courseName: String) {
+ onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(courseName))).scrollTo().click()
+ }
+
+ //OfflineMethod
+ fun expandCollapseItem(itemName: String) {
+ onView(withId(R.id.arrow) + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) + hasSibling(withId(R.id.title) + withText(itemName))).scrollTo().perform(ForceClick())
+ }
+
+ //OfflineMethod
+ fun expandCollapseFiles() {
+ expandCollapseItem("Files")
}
//OfflineMethod
fun clickOnSyncButton() {
- onView(withId(R.id.syncButton)).click()
+ syncButton.click()
confirmSync()
}
@@ -48,4 +72,60 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) {
private fun confirmSync() {
waitForView(withText("Sync") + withAncestor(R.id.buttonPanel)).click()
}
+
+ //OfflineMethod
+ fun confirmDiscardChanges() {
+ waitForView(withText("Discard") + withAncestor(R.id.buttonPanel)).click()
+ }
+
+ //OfflineMethod
+ fun assertStorageInfoDetails() {
+ onView(withId(R.id.storageLabel) + withText(R.string.offline_content_storage)).assertDisplayed()
+ onView(withId(R.id.storageInfo) + containsTextCaseInsensitive("Used")).assertDisplayed()
+ onView(withId(R.id.progress) + withParent(withId(R.id.storageInfoContainer))).assertDisplayed()
+ onView(withId(R.id.otherLabel) + withText(R.string.offline_content_other)).assertDisplayed()
+ onView(withId(R.id.canvasLabel) + withText(R.string.offline_content_canvas_student)).assertDisplayed()
+ onView(withId(R.id.remainingLabel) + withText(R.string.offline_content_remaining)).assertDisplayed()
+ }
+
+ //OfflineMethod
+ fun assertSelectButtonText(selectAll: Boolean) {
+ if(selectAll) waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_select_all)).assertDisplayed()
+ else waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_deselect_all)).assertDisplayed()
+ }
+
+ //OfflineMethod
+ fun clickOnSelectAllButton() {
+ waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_select_all)).click()
+ }
+
+ //OfflineMethod
+ fun clickOnDeselectAllButton() {
+ waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_deselect_all)).click()
+ }
+
+ //OfflineMethod
+ fun assertCourseCountWithMatcher(expectedCount: Int) {
+ ConstraintLayoutItemCountAssertionWithMatcher((allOf(withId(R.id.arrow), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))), expectedCount)
+ }
+
+ //OfflineMethod
+ fun assertCourseCount(expectedCount: Int) {
+ onView((allOf(withId(R.id.arrow), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))).check(ConstraintLayoutItemCountAssertion(expectedCount))
+ }
+
+ //OfflineMethod
+ fun assertToolbarTexts(courseName: String) {
+ onView(withText(courseName) + withParent(R.id.toolbar) + withAncestor(R.id.manageOfflineContentPage)).assertDisplayed()
+ onView(withText(R.string.offline_content_toolbar_title) + withParent(R.id.toolbar) + withAncestor(R.id.manageOfflineContentPage)).assertDisplayed()
+ }
+
+ fun assertCheckedStateOfItem(itemName: String, state: Int) {
+ onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName)) + hasCheckedState(state)).scrollTo().assertDisplayed()
+ }
+
+ fun waitForItemDisappear(itemName: String) {
+ onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName))).check(DoesNotExistAssertion(5))
+ }
+
}
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 001c0a9222..9c0b7c01cb 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
@@ -37,6 +37,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.util.HumanReadables
import androidx.test.espresso.util.TreeIterables
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult
+import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.textfield.TextInputLayout
import com.instructure.espresso.ActivityHelper
import junit.framework.AssertionFailedError
@@ -203,6 +204,30 @@ fun withIndex(matcher: Matcher, index: Int): Matcher {
}
}
+fun withRotation(rotation: Float): Matcher {
+ return object : TypeSafeMatcher() {
+ override fun matchesSafely(item: View): Boolean {
+ return item.rotation == rotation
+ }
+
+ override fun describeTo(description: Description) {
+ description.appendText("with rotation: $rotation")
+ }
+ }
+}
+
+fun hasCheckedState(checkedState: Int) : Matcher {
+ return object : TypeSafeMatcher() {
+ override fun matchesSafely(item: View): Boolean {
+ return item is MaterialCheckBox && item.checkedState == checkedState
+ }
+
+ override fun describeTo(description: Description?) {
+ description?.appendText("has the proper checked state.")
+ }
+ }
+}
+
// A matcher for views whose width is less than the specified amount (in dp),
// but whose height is at least the specified amount.
// This is used to suppress accessibility failures related to overflow menus
diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt
index 92a6c1f62d..0951e8236d 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt
@@ -20,6 +20,7 @@ import android.graphics.Color
import android.view.View
import android.widget.TextView
import androidx.annotation.IdRes
+import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.ViewAssertion
@@ -28,6 +29,7 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.viewpager.widget.ViewPager
import com.google.android.material.bottomnavigation.BottomNavigationView
import junit.framework.AssertionFailedError
+import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.junit.Assert.assertEquals
@@ -93,4 +95,28 @@ class DoesNotExistAssertion(private val timeoutInSeconds: Long, private val poll
throw AssertionError("View still exists after $timeoutInSeconds seconds.")
}
-}
\ No newline at end of file
+}
+
+class ConstraintLayoutItemCountAssertionWithMatcher(private val matcher: Matcher, private val expectedCount: Int) : ViewAssertion {
+ override fun check(view: View, noViewFoundException: NoMatchingViewException?) {
+ noViewFoundException?.let { throw it }
+ if (view !is ConstraintLayout) {
+ throw ClassCastException("View of type ${view.javaClass.simpleName} must be a ConstraintLayout")
+ }
+ val count = (0 until view.childCount)
+ .map { view.getChildAt(it) }.count { matcher.matches(it) }
+ ViewMatchers.assertThat(count, Matchers.`is`(expectedCount))
+ }
+}
+
+class ConstraintLayoutItemCountAssertion(private val expectedCount: Int) : ViewAssertion {
+ override fun check(view: View, noViewFoundException: NoMatchingViewException?) {
+ noViewFoundException?.let { throw it }
+ if (view !is ConstraintLayout) {
+ throw ClassCastException("View of type ${view.javaClass.simpleName} must be a ConstraintLayout")
+ }
+ val count = view.childCount
+ ViewMatchers.assertThat(count, Matchers.`is`(expectedCount))
+ }
+}
+
diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/actions/ForceClick.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/actions/ForceClick.kt
new file mode 100644
index 0000000000..a69bc625b9
--- /dev/null
+++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/actions/ForceClick.kt
@@ -0,0 +1,50 @@
+//
+// Copyright (C) 2023-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.actions
+
+
+import android.view.View
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.matcher.ViewMatchers.isClickable
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isEnabled
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.Matcher
+
+
+/**
+ * During Espresso click the coordinates calculation may have been broken by setRotation call on the view.
+ * This forceClick will perform click without checking the coordinates.
+ *
+ */
+class ForceClick : ViewAction {
+
+ override fun getConstraints(): Matcher? {
+ return allOf(isClickable(), isEnabled(), isDisplayed())
+ }
+
+ override fun getDescription(): String? {
+ return "force click"
+ }
+
+ override fun perform(uiController: UiController, view: View) {
+ view.performClick() // perform click without checking view coordinates.
+ uiController.loopMainThreadUntilIdle()
+ }
+
+}
diff --git a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt
index 4f5a8ad474..7b7909c6e6 100644
--- a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt
+++ b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt
@@ -34,7 +34,7 @@ enum class Priority {
enum class FeatureCategory {
ASSIGNMENTS, SUBMISSIONS, LOGIN, COURSE, DASHBOARD, GROUPS, SETTINGS, PAGES, DISCUSSIONS, MODULES,
INBOX, GRADES, FILES, EVENTS, PEOPLE, CONFERENCES, COLLABORATIONS, SYLLABUS, TODOS, QUIZZES, NOTIFICATIONS,
- ANNOTATIONS, ANNOUNCEMENTS, COMMENTS, BOOKMARKS, NONE, K5_DASHBOARD, SPEED_GRADER, SYNC_SETTINGS, SYNC_PROGRESS
+ ANNOTATIONS, ANNOUNCEMENTS, COMMENTS, BOOKMARKS, NONE, K5_DASHBOARD, SPEED_GRADER, SYNC_SETTINGS, SYNC_PROGRESS, OFFLINE_CONTENT
}
enum class SecondaryFeatureCategory {
From 9744113913fd226994a24e526a19b91e2ae38f8b Mon Sep 17 00:00:00 2001
From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com>
Date: Thu, 2 Nov 2023 13:05:56 +0100
Subject: [PATCH 03/19] [MBL-17141][Teacher] Canvas Studio global navigation
displays the old Arc icon
refs: MBL-17141
affects: Teacher
release note: none
---
.../src/main/res/layout/navigation_drawer.xml | 2 +-
.../main/res/drawable/ic_navigation_arc.xml | 28 -------------------
2 files changed, 1 insertion(+), 29 deletions(-)
delete mode 100644 libs/pandares/src/main/res/drawable/ic_navigation_arc.xml
diff --git a/apps/teacher/src/main/res/layout/navigation_drawer.xml b/apps/teacher/src/main/res/layout/navigation_drawer.xml
index 57de755a08..de3f459f15 100644
--- a/apps/teacher/src/main/res/layout/navigation_drawer.xml
+++ b/apps/teacher/src/main/res/layout/navigation_drawer.xml
@@ -119,7 +119,7 @@
+ app:srcCompat="@drawable/ic_navigation_studio" />
-
-
-
-
-
\ No newline at end of file
From 6d832fae1ab2c380bbac07ad75ec1752c4cb2326 Mon Sep 17 00:00:00 2001
From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com>
Date: Thu, 2 Nov 2023 15:41:01 +0100
Subject: [PATCH 04/19] [MBL-17143][Student][Teacher] JS events are not
responsive when we have an lti or google docs iframe in the discussion reply
#2225
refs: MBL-17143
affects: Student, Teacher
release note: Fixed a bug where the discussion reply actions were not working in some cases.
---
.../main/assets/discussion_topic_header_html_template.html | 2 +-
.../assets/discussion_topic_header_html_template_rtl.html | 2 +-
.../com/instructure/pandautils/binding/BindingAdapters.kt | 4 ++--
.../com/instructure/pandautils/utils/HtmlContentFormatter.kt | 2 +-
.../com/instructure/pandautils/utils/WebViewExtensions.kt | 4 ++--
5 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/libs/pandautils/src/main/assets/discussion_topic_header_html_template.html b/libs/pandautils/src/main/assets/discussion_topic_header_html_template.html
index 076ac0b643..b2b9e8c605 100644
--- a/libs/pandautils/src/main/assets/discussion_topic_header_html_template.html
+++ b/libs/pandautils/src/main/assets/discussion_topic_header_html_template.html
@@ -29,7 +29,7 @@