chore(ui): migrate LocalClipboardManager to LocalClipboard #286
@@ -1,12 +1,14 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.ui.reporting
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
@@ -17,6 +19,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -123,6 +126,23 @@ class ReportReviewScreenTest {
|
||||
composeTestRule.onNodeWithText(string(R.string.report_submit)).performScrollTo().assertIsEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tappingCopy_putsThePayloadOnTheSystemClipboard_andShowsAConfirmation() {
|
||||
// Exercises the #237 migration off LocalClipboardManager/ClipboardManager end-to-end: the
|
||||
// real system clipboard (not a fake) must contain the exact payload after the suspend
|
||||
// LocalClipboard/Clipboard call completes.
|
||||
val store = setContent()
|
||||
|
||||
composeTestRule.onNodeWithText(string(R.string.report_copy)).performScrollTo().performClick()
|
||||
|
||||
composeTestRule.waitUntil(5_000) {
|
||||
composeTestRule.onAllNodesWithText(string(R.string.report_copied)).fetchSemanticsNodes().isNotEmpty()
|
||||
}
|
||||
val clipboardManager = context.getSystemService(ClipboardManager::class.java)
|
||||
val clipText = clipboardManager?.primaryClip?.getItemAt(0)?.text?.toString()
|
||||
assertEquals(store.find(reportId)!!.toSubmissionPayload(), clipText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tappingDiscard_deletesReport_andLeavesTheScreen() {
|
||||
var done = false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.ui.reporting
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -39,10 +40,11 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.Clipboard
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
@@ -57,7 +59,7 @@ import org.libremail.R
|
||||
fun ReportReviewScreen(onDone: () -> Unit, viewModel: ReportReviewViewModel = hiltViewModel()) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val clipboard = LocalClipboard.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
// Latches once the post-submit confirmation dialog is acknowledged, so it can't reappear
|
||||
@@ -188,8 +190,10 @@ fun ReportReviewScreen(onDone: () -> Unit, viewModel: ReportReviewViewModel = hi
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
clipboard.setText(AnnotatedString(viewModel.payload()))
|
||||
scope.launch { snackbarHostState.showSnackbar(copiedMessage) }
|
||||
scope.launch {
|
||||
copyReportPayloadToClipboard(clipboard, viewModel.payload())
|
||||
snackbarHostState.showSnackbar(copiedMessage)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
@@ -228,6 +232,21 @@ fun ReportReviewScreen(onDone: () -> Unit, viewModel: ReportReviewViewModel = hi
|
||||
}
|
||||
}
|
||||
|
||||
/** Label attached to the clip so OS-level clipboard UI (e.g. clipboard history) can identify it. */
|
||||
private const val REPORT_CLIP_LABEL = "LibreMail report"
|
||||
|
||||
/**
|
||||
* Copies [payload] to the system clipboard as plain text via the suspend [Clipboard] API (#237 —
|
||||
* `LocalClipboardManager`/`ClipboardManager` are deprecated in favor of `LocalClipboard`).
|
||||
*
|
||||
* Kept as a plain suspend function rather than inlined in the composable so the "Copy report"
|
||||
* action's clipboard interaction is unit-testable directly against a fake/mock [Clipboard],
|
||||
* without a Compose UI test or emulator.
|
||||
*/
|
||||
internal suspend fun copyReportPayloadToClipboard(clipboard: Clipboard, payload: String) {
|
||||
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(REPORT_CLIP_LABEL, payload)))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PiiDisclaimer() {
|
||||
Surface(
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.ui.reporting
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.ui.platform.Clipboard
|
||||
import io.mockk.Runs
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Unit-level coverage for [copyReportPayloadToClipboard] (#237: `LocalClipboardManager`/
|
||||
* `ClipboardManager` — deprecated in favor of the suspend `LocalClipboard`/[Clipboard] API — were
|
||||
* migrated off of here). The "Copy report" action's clipboard interaction is exercised directly
|
||||
* against a mocked [Clipboard], with [ClipData]'s static factory mocked the same way
|
||||
* [org.libremail.contacts.ContactsPermissionManagerTest] mocks `Uri` — so this runs as a plain JVM
|
||||
* unit test, no Compose UI test or emulator needed.
|
||||
*/
|
||||
class ReportReviewClipboardTest {
|
||||
|
||||
private val clipboard = mockk<Clipboard>()
|
||||
private val clipData = mockk<ClipData>()
|
||||
private val textSlot = slot<CharSequence>()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkStatic(ClipData::class)
|
||||
every { ClipData.newPlainText(any(), capture(textSlot)) } returns clipData
|
||||
coEvery { clipboard.setClipEntry(any()) } just Runs
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkStatic(ClipData::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `copying the payload builds a plain-text clip from the exact text and sets it`() = runTest {
|
||||
copyReportPayloadToClipboard(clipboard, "the report body")
|
||||
|
||||
assertEquals("the report body", textSlot.captured.toString())
|
||||
coVerify(exactly = 1) { clipboard.setClipEntry(match { it.clipData === clipData }) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user