Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@ class FileUploadDialogFragment : BaseCanvasDialogFragment() {
fileUploadEventHandler.postEvent(
FileUploadEvent.UploadStarted(
action.id,
action.liveData
action.liveData,
action.selectedUris
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ sealed class FileUploadEvent {
data class FileSelected(val filePaths: List<String>) : FileUploadEvent()
data class UploadStarted(
val uuid: UUID?,
val workInfoLiveData: LiveData<WorkInfo>
val workInfoLiveData: LiveData<WorkInfo>,
val filePaths: List<String>
) : FileUploadEvent()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,16 +208,20 @@ fun SpeedGraderCommentsSection(
if (state.fileSelectorDialogData != null) {
val fragmentManager = LocalContext.current.getFragmentActivity().supportFragmentManager

val bundle = FileUploadDialogFragment.createTeacherSubmissionCommentBundle(
state.fileSelectorDialogData.courseId,
state.fileSelectorDialogData.assignmentId,
state.fileSelectorDialogData.userId,
state.fileSelectorDialogData.attempt
)
// Check if dialog is already showing to prevent duplicates
val existingDialog = fragmentManager.findFragmentByTag(FileUploadDialogFragment.TAG)
if (existingDialog == null) {
val bundle = FileUploadDialogFragment.createTeacherSubmissionCommentBundle(
state.fileSelectorDialogData.courseId,
state.fileSelectorDialogData.assignmentId,
state.fileSelectorDialogData.userId,
state.fileSelectorDialogData.attempt
)

FileUploadDialogFragment.newInstance(bundle).show(
fragmentManager, FileUploadDialogFragment.TAG + UUID.randomUUID()
)
FileUploadDialogFragment.newInstance(bundle).show(
fragmentManager, FileUploadDialogFragment.TAG
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ class SpeedGraderCommentsViewModel @Inject constructor(
fileUploadEventHandler.events.collect { event ->
when (event) {
is FileUploadEvent.UploadStarted -> {
onFileUploadStarted(event.workInfoLiveData)
onFileUploadStarted(event.workInfoLiveData, event.filePaths)
}
is FileUploadEvent.FileSelected -> {
selectedFilePaths = event.filePaths
Expand Down Expand Up @@ -347,7 +347,7 @@ class SpeedGraderCommentsViewModel @Inject constructor(
}

is SpeedGraderCommentsAction.FileUploadStarted -> {
onFileUploadStarted(action.workInfoLiveData)
onFileUploadStarted(action.workInfoLiveData, selectedFilePaths.orEmpty())
}

is SpeedGraderCommentsAction.FilesSelected -> {
Expand All @@ -356,7 +356,7 @@ class SpeedGraderCommentsViewModel @Inject constructor(
}
}

private fun onFileUploadStarted(workInfoLiveData: LiveData<WorkInfo>) {
private fun onFileUploadStarted(workInfoLiveData: LiveData<WorkInfo>, filePaths: List<String>) {
_uiState.update { state ->
state.copy(
fileSelectorDialogData = null,
Expand All @@ -366,8 +366,8 @@ class SpeedGraderCommentsViewModel @Inject constructor(
// Subscribe to the worker's LiveData to observe its state
viewModelScope.launch {
workInfoLiveData.asFlow().collect { workInfo ->
when (workInfo.state) {
WorkInfo.State.RUNNING -> createPendingFileComment(workInfo)
when (workInfo?.state) {
WorkInfo.State.RUNNING -> createPendingFileComment(workInfo, filePaths)
WorkInfo.State.SUCCEEDED -> handleFileUploadSuccess(workInfo)
WorkInfo.State.FAILED -> handleFileUploadFailure(workInfo)
else -> {}
Expand All @@ -376,15 +376,15 @@ class SpeedGraderCommentsViewModel @Inject constructor(
}
}

private suspend fun createPendingFileComment(workInfo: WorkInfo) {
private suspend fun createPendingFileComment(workInfo: WorkInfo, filePaths: List<String>) {
var fileUploadInput = fileUploadInputDao.findByWorkerId(workInfo.id.toString())
if (fileUploadInput == null) {
fileUploadInput = FileUploadInputEntity(
workerId = workInfo.id.toString(),
courseId = courseId,
assignmentId = assignmentId,
userId = studentId,
filePaths = selectedFilePaths.orEmpty(),
filePaths = filePaths,
action = FileUploadWorker.ACTION_TEACHER_SUBMISSION_COMMENT,
attemptId = selectedAttemptId.takeIf { assignmentEnhancementsEnabled }
)
Expand All @@ -399,7 +399,7 @@ class SpeedGraderCommentsViewModel @Inject constructor(
this.workerId = workInfo.id
this.status = CommentSendStatus.SENDING
this.workerInputData = FileUploadWorkerData(
selectedFilePaths.orEmpty(),
filePaths,
courseId,
assignmentId,
studentId
Expand Down Expand Up @@ -647,7 +647,7 @@ class SpeedGraderCommentsViewModel @Inject constructor(
fileUploadInputDao.insert(inputData)

WorkManager.getInstance(context).apply {
onFileUploadStarted(getWorkInfoByIdLiveData(worker.id))
onFileUploadStarted(getWorkInfoByIdLiveData(worker.id), filePaths)
enqueue(worker)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
package com.instructure.pandautils.features.speedgrader.grade.comments

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.work.WorkInfo
import com.instructure.canvasapi2.SubmissionCommentsQuery
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.pandautils.features.file.upload.FileUploadEvent
import com.instructure.pandautils.features.file.upload.FileUploadEventHandler
import com.instructure.pandautils.features.speedgrader.SpeedGraderSelectedAttemptHolder
import com.instructure.pandautils.room.appdatabase.daos.AttachmentDao
Expand All @@ -28,19 +31,23 @@ import com.instructure.pandautils.room.appdatabase.daos.FileUploadInputDao
import com.instructure.pandautils.room.appdatabase.daos.MediaCommentDao
import com.instructure.pandautils.room.appdatabase.daos.PendingSubmissionCommentDao
import com.instructure.pandautils.room.appdatabase.daos.SubmissionCommentDao
import com.instructure.pandautils.room.appdatabase.entities.FileUploadInputEntity
import com.instructure.pandautils.room.appdatabase.entities.PendingSubmissionCommentEntity
import com.instructure.pandautils.room.appdatabase.model.PendingSubmissionCommentWithFileUploadInput
import com.instructure.pandautils.views.RecordingMediaType
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
Expand All @@ -49,6 +56,7 @@ import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.UUID

@ExperimentalCoroutinesApi
class SpeedGraderCommentsViewModelTest {
Expand Down Expand Up @@ -372,4 +380,194 @@ class SpeedGraderCommentsViewModelTest {
Thread.sleep(100)
assertEquals(0, viewModel.uiState.value.comments.size)
}

@Test
fun `FileUploadEvent UploadStarted uses file paths from event`() = runTest {
val fileUploadEventsFlow = MutableSharedFlow<FileUploadEvent>(replay = 1)
coEvery { fileUploadEventHandler.events } returns fileUploadEventsFlow

val workInfoLiveData = MutableLiveData<WorkInfo>()
val workInfo = mockk<WorkInfo>(relaxed = true)
val workerId = UUID.randomUUID()
every { workInfo.id } returns workerId
every { workInfo.state } returns WorkInfo.State.RUNNING

val expectedFilePaths = listOf("/path/to/file1.pdf", "/path/to/file2.jpg")
val fileUploadInputSlot = slot<FileUploadInputEntity>()

coEvery { fileUploadInputDao.findByWorkerId(any()) } returns null
coEvery { fileUploadInputDao.insert(capture(fileUploadInputSlot)) } returns Unit
coEvery { pendingSubmissionCommentDao.findByPageId(any()) } returns null
coEvery { pendingSubmissionCommentDao.insert(any()) } returns 1L

createViewModel()

// Emit the UploadStarted event with file paths
fileUploadEventsFlow.emit(
FileUploadEvent.UploadStarted(
uuid = workerId,
workInfoLiveData = workInfoLiveData,
filePaths = expectedFilePaths
)
)

advanceUntilIdle()

// Trigger the worker state change
workInfoLiveData.postValue(workInfo)

advanceUntilIdle()

// Verify that FileUploadInputEntity was created with the correct file paths from the event
coVerify { fileUploadInputDao.insert(any()) }
assertEquals(expectedFilePaths, fileUploadInputSlot.captured.filePaths)
}

@Test
fun `FileUploadEvent UploadStarted creates pending comment with correct file paths`() = runTest {
val fileUploadEventsFlow = MutableSharedFlow<FileUploadEvent>(replay = 1)
coEvery { fileUploadEventHandler.events } returns fileUploadEventsFlow

val workInfoLiveData = MutableLiveData<WorkInfo>()
val workInfo = mockk<WorkInfo>(relaxed = true)
val workerId = UUID.randomUUID()
every { workInfo.id } returns workerId
every { workInfo.state } returns WorkInfo.State.RUNNING

val expectedFilePaths = listOf("/path/to/file1.pdf")
val pendingCommentSlot = slot<PendingSubmissionCommentEntity>()

coEvery { fileUploadInputDao.findByWorkerId(any()) } returns null
coEvery { fileUploadInputDao.insert(any()) } returns Unit
coEvery { pendingSubmissionCommentDao.findByPageId(any()) } returns null
coEvery { pendingSubmissionCommentDao.insert(capture(pendingCommentSlot)) } returns 1L

createViewModel()

// Emit the UploadStarted event
fileUploadEventsFlow.emit(
FileUploadEvent.UploadStarted(
uuid = workerId,
workInfoLiveData = workInfoLiveData,
filePaths = expectedFilePaths
)
)

advanceUntilIdle()

// Trigger worker state
workInfoLiveData.postValue(workInfo)

advanceUntilIdle()

// Verify pending comment was created with the correct file paths
coVerify { pendingSubmissionCommentDao.insert(any()) }
// The workerInputData is not stored directly in the entity - it's constructed from fileUploadInput
// So we can't test it here. Instead, we verify that the entity was created successfully.
assertEquals("domain-3-1-2", pendingCommentSlot.captured.pageId)
}

@Test
fun `FileUploadEvent UploadStarted does not create duplicate pending comments`() = runTest {
val fileUploadEventsFlow = MutableSharedFlow<FileUploadEvent>(replay = 1)
coEvery { fileUploadEventHandler.events } returns fileUploadEventsFlow

val workInfoLiveData = MutableLiveData<WorkInfo>()
val workInfo = mockk<WorkInfo>(relaxed = true)
val workerId = UUID.randomUUID()
every { workInfo.id } returns workerId
every { workInfo.state } returns WorkInfo.State.RUNNING

val filePaths = listOf("/path/to/file.pdf")
val existingFileUploadInput = FileUploadInputEntity(
workerId = workerId.toString(),
filePaths = filePaths,
courseId = 3L,
assignmentId = 1L,
userId = 2L,
action = "teacher_submission_comment"
)

val existingPendingComment = PendingSubmissionCommentWithFileUploadInput(
pendingSubmissionCommentEntity = PendingSubmissionCommentEntity(
pageId = "domain-3-1-2"
),
fileUploadInput = existingFileUploadInput
)

coEvery { fileUploadInputDao.findByWorkerId(workerId.toString()) } returns existingFileUploadInput
coEvery { pendingSubmissionCommentDao.findByPageId(any()) } returns listOf(existingPendingComment)

createViewModel()

// Emit the UploadStarted event
fileUploadEventsFlow.emit(
FileUploadEvent.UploadStarted(
uuid = workerId,
workInfoLiveData = workInfoLiveData,
filePaths = filePaths
)
)

advanceUntilIdle()

// Trigger worker state
workInfoLiveData.postValue(workInfo)

advanceUntilIdle()

// Verify no duplicate was created
coVerify(exactly = 0) { fileUploadInputDao.insert(any()) }
coVerify(exactly = 0) { pendingSubmissionCommentDao.insert(any()) }
}

@Test
fun `FileSelected event updates selectedFilePaths variable`() = runTest {
val fileUploadEventsFlow = MutableSharedFlow<FileUploadEvent>(replay = 1)
coEvery { fileUploadEventHandler.events } returns fileUploadEventsFlow

createViewModel()

val expectedFilePaths = listOf("/path/to/selected/file.pdf")

// Emit FileSelected event
fileUploadEventsFlow.emit(
FileUploadEvent.FileSelected(filePaths = expectedFilePaths)
)

advanceUntilIdle()

// The internal selectedFilePaths variable should be updated
// This is tested indirectly by ensuring the next upload uses these paths
viewModel.handleAction(
SpeedGraderCommentsAction.FileUploadStarted(
workInfoLiveData = MutableLiveData()
)
)

advanceUntilIdle()

// Verify the action was handled (the implementation uses selectedFilePaths)
Assert.assertFalse(viewModel.uiState.value.showAttachmentTypeDialog)
}

@Test
fun `DialogDismissed event clears file selector dialog`() = runTest {
val fileUploadEventsFlow = MutableSharedFlow<FileUploadEvent>(replay = 1)
coEvery { fileUploadEventHandler.events } returns fileUploadEventsFlow

createViewModel()

// First show the dialog
viewModel.handleAction(SpeedGraderCommentsAction.ChooseFilesClicked)
Assert.assertNotNull(viewModel.uiState.value.fileSelectorDialogData)

// Emit DialogDismissed event
fileUploadEventsFlow.emit(FileUploadEvent.DialogDismissed)

advanceUntilIdle()

// Verify dialog data is cleared
Assert.assertNull(viewModel.uiState.value.fileSelectorDialogData)
}
}
Loading