test(shared): resolve UI copy from resources

This commit is contained in:
2026-07-25 19:46:34 +02:00
parent c7657c4b37
commit 31ba3f40b2

View File

@@ -63,10 +63,61 @@ import com.vnidrop.app.ui.platform.LocalUiPlatform
import com.vnidrop.app.ui.shell.AppShell import com.vnidrop.app.ui.shell.AppShell
import com.vnidrop.app.ui.theme.VniDropTheme import com.vnidrop.app.ui.theme.VniDropTheme
import com.vnidrop.app.ui.theme.LocalVniDropColors import com.vnidrop.app.ui.theme.LocalVniDropColors
import kotlinx.coroutines.runBlocking
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import vnidrop.shared.generated.resources.Res
import vnidrop.shared.generated.resources.about_is_direct
import vnidrop.shared.generated.resources.about_is_title
import vnidrop.shared.generated.resources.about_isnt_title
import vnidrop.shared.generated.resources.about_privacy_title
import vnidrop.shared.generated.resources.about_tagline
import vnidrop.shared.generated.resources.approval_endpoint_id
import vnidrop.shared.generated.resources.button_approve
import vnidrop.shared.generated.resources.button_choose_files
import vnidrop.shared.generated.resources.button_close
import vnidrop.shared.generated.resources.button_create_new_transfer
import vnidrop.shared.generated.resources.button_download_invitation
import vnidrop.shared.generated.resources.button_open_settings
import vnidrop.shared.generated.resources.button_receive_files
import vnidrop.shared.generated.resources.nav_receive
import vnidrop.shared.generated.resources.nav_send
import vnidrop.shared.generated.resources.notifications_description
import vnidrop.shared.generated.resources.notifications_local_title
import vnidrop.shared.generated.resources.notifications_title
import vnidrop.shared.generated.resources.receive_choose_method_title
import vnidrop.shared.generated.resources.receive_clear_history
import vnidrop.shared.generated.resources.receive_clear_history_description
import vnidrop.shared.generated.resources.receive_clear_history_title
import vnidrop.shared.generated.resources.receive_delete_history_item
import vnidrop.shared.generated.resources.receive_empty_title
import vnidrop.shared.generated.resources.receive_method_file
import vnidrop.shared.generated.resources.receive_new_subtitle
import vnidrop.shared.generated.resources.relay_add_url
import vnidrop.shared.generated.resources.relay_apply
import vnidrop.shared.generated.resources.relay_mode_custom
import vnidrop.shared.generated.resources.relay_strict_warning
import vnidrop.shared.generated.resources.send_access_anyone
import vnidrop.shared.generated.resources.send_choose_file_title
import vnidrop.shared.generated.resources.send_subtitle
import vnidrop.shared.generated.resources.settings_network_title
import vnidrop.shared.generated.resources.settings_subtitle
import vnidrop.shared.generated.resources.snackbar_dismiss
import vnidrop.shared.generated.resources.status_available
import vnidrop.shared.generated.resources.storage_calculating
import vnidrop.shared.generated.resources.storage_clear_transfer_cache
import vnidrop.shared.generated.resources.storage_clear_transfer_cache_description
import vnidrop.shared.generated.resources.storage_delete_transfers
import vnidrop.shared.generated.resources.storage_delete_transfers_description
import vnidrop.shared.generated.resources.storage_received_files
import vnidrop.shared.generated.resources.storage_transfer_data
import vnidrop.shared.generated.resources.transfer_qr_unavailable
import vnidrop.shared.generated.resources.transfer_scan_qr
import vnidrop.shared.generated.resources.transfer_share_title
@OptIn(ExperimentalTestApi::class) @OptIn(ExperimentalTestApi::class)
class FoundationComposeTest { class FoundationComposeTest {
@@ -82,7 +133,7 @@ class FoundationComposeTest {
) )
} }
} }
onNodeWithText("Approve").performClick() onNodeWithText(Res.string.button_approve.value).performClick()
runOnIdle { assertEquals("request", accepted) } runOnIdle { assertEquals("request", accepted) }
} }
@@ -111,8 +162,8 @@ class FoundationComposeTest {
) )
} }
} }
onNodeWithText("Notifications").performClick() onNodeWithText(Res.string.notifications_title.value).performClick()
onNodeWithText("Get notified about transfer activity while VniDrop is in the background.").assertIsDisplayed() onNodeWithText(Res.string.notifications_description.value).assertIsDisplayed()
} }
@Test @Test
@@ -162,15 +213,12 @@ class FoundationComposeTest {
} }
} }
onNodeWithText("Network").performClick() onNodeWithText(Res.string.settings_network_title.value).performClick()
onNodeWithText("Device ID: endpoint-for-allowlist").assertIsDisplayed() onNodeWithText(Res.string.approval_endpoint_id.value("endpoint-for-allowlist")).assertIsDisplayed()
onNodeWithText("Strict custom").performClick() onNodeWithText(Res.string.relay_mode_custom.value).performClick()
onNodeWithText( onNodeWithText(Res.string.relay_strict_warning.value).assertIsDisplayed()
"Strict custom mode will not start unless at least one configured relay is reachable. " + onNodeWithText(Res.string.relay_add_url.value).assertIsDisplayed()
"VniDrop never uses public relays or public discovery in this mode.", onNodeWithText(Res.string.relay_apply.value).performClick()
).assertIsDisplayed()
onNodeWithText("Add relay server").assertIsDisplayed()
onNodeWithText("Apply network settings").performClick()
runOnIdle { assertTrue(applied) } runOnIdle { assertTrue(applied) }
} }
@@ -203,20 +251,14 @@ class FoundationComposeTest {
} }
} }
onNodeWithText("Clear transfer cache").performClick() onNodeWithText(Res.string.storage_clear_transfer_cache.value).performClick()
onNodeWithText( onNodeWithText(Res.string.storage_clear_transfer_cache_description.value).assertIsDisplayed()
"Removes cached transfer content after briefly restarting VniDrop. " +
"Finish ongoing transfers and stop active shares first. Received files and transfer history are not deleted.",
).assertIsDisplayed()
runOnIdle { assertFalse(cacheClearRequested) } runOnIdle { assertFalse(cacheClearRequested) }
onNodeWithTag("confirm-clear-transfer-cache").performClick() onNodeWithTag("confirm-clear-transfer-cache").performClick()
runOnIdle { assertTrue(cacheClearRequested) } runOnIdle { assertTrue(cacheClearRequested) }
onNodeWithText("Delete all transfers").performClick() onNodeWithText(Res.string.storage_delete_transfers.value).performClick()
onNodeWithText( onNodeWithText(Res.string.storage_delete_transfers_description.value).assertIsDisplayed()
"This clears all sent and received transfer records from your history and immediately reclaims unused transfer cache. " +
"Ongoing transfers and received files are not deleted. This cant be undone.",
).assertIsDisplayed()
runOnIdle { assertFalse(deleteRequested) } runOnIdle { assertFalse(deleteRequested) }
onNodeWithTag("confirm-delete-all-transfers").performClick() onNodeWithTag("confirm-delete-all-transfers").performClick()
@@ -260,9 +302,9 @@ class FoundationComposeTest {
} }
} }
onNodeWithText("Received files").assertIsDisplayed() onNodeWithText(Res.string.storage_received_files.value).assertIsDisplayed()
onNodeWithText("Transfer data").assertIsDisplayed() onNodeWithText(Res.string.storage_transfer_data.value).assertIsDisplayed()
onAllNodesWithText("Calculating storage usage…").assertCountEquals(0) onAllNodesWithText(Res.string.storage_calculating.value).assertCountEquals(0)
} }
@Test @Test
@@ -292,14 +334,12 @@ class FoundationComposeTest {
} }
} }
onNodeWithText("Send files directly. Stay in control of who receives them.").assertIsDisplayed() onNodeWithText(Res.string.about_tagline.value).assertIsDisplayed()
onNodeWithText("What VniDrop is").assertIsDisplayed() onNodeWithText(Res.string.about_is_title.value).assertIsDisplayed()
onNodeWithText("What VniDrop isnt").assertIsDisplayed() onNodeWithText(Res.string.about_isnt_title.value).assertIsDisplayed()
onAllNodesWithText("Privacy & security").assertCountEquals(1) onAllNodesWithText(Res.string.about_privacy_title.value).assertCountEquals(1)
onAllNodesWithText("Apache 2.0").assertCountEquals(1) onAllNodesWithText("Apache 2.0").assertCountEquals(1)
val explanationBounds = onNodeWithText( val explanationBounds = onNodeWithText(Res.string.about_is_direct.value).getUnclippedBoundsInRoot()
"A direct device-to-device transfer — your files go straight to the receiver.",
).getUnclippedBoundsInRoot()
assertTrue(explanationBounds.bottom - explanationBounds.top > 32.dp) assertTrue(explanationBounds.bottom - explanationBounds.top > 32.dp)
} }
@@ -328,7 +368,7 @@ class FoundationComposeTest {
) )
} }
} }
onNodeWithText("Allow notifications").performClick() onNodeWithText(Res.string.notifications_local_title.value).performClick()
runOnIdle { assertEquals(true, enabled) } runOnIdle { assertEquals(true, enabled) }
} }
@@ -360,7 +400,7 @@ class FoundationComposeTest {
) )
} }
} }
onNodeWithText("Open Settings").performClick() onNodeWithText(Res.string.button_open_settings.value).performClick()
runOnIdle { assertTrue(opened) } runOnIdle { assertTrue(opened) }
} }
@@ -372,7 +412,7 @@ class FoundationComposeTest {
VniDropTheme(isDarkTheme = false) { VniDropSnackbarHost(controller) } VniDropTheme(isDarkTheme = false) { VniDropSnackbarHost(controller) }
} }
onNodeWithText("Saved successfully").assertIsDisplayed() onNodeWithText("Saved successfully").assertIsDisplayed()
onNodeWithContentDescription("Dismiss").assertIsDisplayed() onNodeWithContentDescription(Res.string.snackbar_dismiss.value).assertIsDisplayed()
} }
@Test @Test
@@ -392,7 +432,7 @@ class FoundationComposeTest {
val messageBottom = onNodeWithText("Notifications are turned off for VniDrop. You can enable them in Settings.") val messageBottom = onNodeWithText("Notifications are turned off for VniDrop. You can enable them in Settings.")
.getUnclippedBoundsInRoot().bottom .getUnclippedBoundsInRoot().bottom
val closeBottom = onNodeWithContentDescription("Dismiss").getUnclippedBoundsInRoot().bottom val closeBottom = onNodeWithContentDescription(Res.string.snackbar_dismiss.value).getUnclippedBoundsInRoot().bottom
val actionTop = onNodeWithText("Open Settings").getUnclippedBoundsInRoot().top val actionTop = onNodeWithText("Open Settings").getUnclippedBoundsInRoot().top
assertTrue(messageBottom <= actionTop) assertTrue(messageBottom <= actionTop)
assertTrue(closeBottom <= actionTop) assertTrue(closeBottom <= actionTop)
@@ -421,7 +461,7 @@ class FoundationComposeTest {
val overlayBottom = onNodeWithTag("snackbar-overlay").getUnclippedBoundsInRoot().bottom val overlayBottom = onNodeWithTag("snackbar-overlay").getUnclippedBoundsInRoot().bottom
val floatingActionTop = onNodeWithTag("floating-action").getUnclippedBoundsInRoot().top val floatingActionTop = onNodeWithTag("floating-action").getUnclippedBoundsInRoot().top
val navigationLabelTop = onNodeWithText("Send").getUnclippedBoundsInRoot().top val navigationLabelTop = onNodeWithText(Res.string.nav_send.value).getUnclippedBoundsInRoot().top
assertTrue(overlayBottom <= floatingActionTop) assertTrue(overlayBottom <= floatingActionTop)
assertTrue(overlayBottom <= navigationLabelTop) assertTrue(overlayBottom <= navigationLabelTop)
} }
@@ -445,7 +485,7 @@ class FoundationComposeTest {
} }
onNodeWithText("VniDrop").assertIsDisplayed() onNodeWithText("VniDrop").assertIsDisplayed()
onNodeWithText("Receive").performClick() onNodeWithText(Res.string.nav_receive.value).performClick()
runOnIdle { assertEquals(AppDestination.Receive, selected) } runOnIdle { assertEquals(AppDestination.Receive, selected) }
} }
@@ -563,9 +603,9 @@ class FoundationComposeTest {
onNodeWithTag("send-empty-icon").assertIsDisplayed() onNodeWithTag("send-empty-icon").assertIsDisplayed()
onNodeWithTag("receive-empty-icon").assertIsDisplayed() onNodeWithTag("receive-empty-icon").assertIsDisplayed()
onAllNodesWithText("Transfers youre sharing from this device.").assertCountEquals(0) onAllNodesWithText(Res.string.send_subtitle.value).assertCountEquals(0)
onAllNodesWithText("Transfers youve received on this device.").assertCountEquals(0) onAllNodesWithText(Res.string.receive_new_subtitle.value).assertCountEquals(0)
onAllNodesWithText("Your name, where transfers are saved, appearance, and notifications.").assertCountEquals(0) onAllNodesWithText(Res.string.settings_subtitle.value).assertCountEquals(0)
} }
@Test @Test
@@ -592,9 +632,9 @@ class FoundationComposeTest {
} }
} }
onNodeWithText("New transfer").performClick() onNodeWithText(Res.string.button_create_new_transfer.value).performClick()
onNodeWithText("Choose what to share").assertIsDisplayed() onNodeWithText(Res.string.send_choose_file_title.value).assertIsDisplayed()
onNodeWithText("Choose files").assertIsDisplayed() onNodeWithText(Res.string.button_choose_files.value).assertIsDisplayed()
} }
@Test @Test
@@ -627,7 +667,7 @@ class FoundationComposeTest {
} }
onNodeWithText("1.5 KB").assertIsDisplayed() onNodeWithText("1.5 KB").assertIsDisplayed()
onNodeWithText("Anyone with this transfer").performClick() onNodeWithText(Res.string.send_access_anyone.value).performClick()
runOnIdle { assertEquals(ShareAccessPolicy.AnyoneWithTransfer, selectedPolicy) } runOnIdle { assertEquals(ShareAccessPolicy.AnyoneWithTransfer, selectedPolicy) }
} }
@@ -656,7 +696,7 @@ class FoundationComposeTest {
} }
val titleBounds = onNodeWithText("Photos").getUnclippedBoundsInRoot() val titleBounds = onNodeWithText("Photos").getUnclippedBoundsInRoot()
val statusBounds = onNodeWithText("Available").getUnclippedBoundsInRoot() val statusBounds = onNodeWithText(Res.string.status_available.value).getUnclippedBoundsInRoot()
assertTrue(statusBounds.left - titleBounds.right <= 12.dp) assertTrue(statusBounds.left - titleBounds.right <= 12.dp)
onNodeWithText("Photos").performClick() onNodeWithText("Photos").performClick()
runOnIdle { assertEquals(9UL, selectedId) } runOnIdle { assertEquals(9UL, selectedId) }
@@ -679,16 +719,16 @@ class FoundationComposeTest {
} }
} }
onNodeWithContentDescription("Share").assertIsDisplayed() onNodeWithContentDescription(Res.string.transfer_share_title.value).assertIsDisplayed()
onAllNodesWithText("Scan with VniDrop to receive this transfer").assertCountEquals(0) onAllNodesWithText(Res.string.transfer_scan_qr.value).assertCountEquals(0)
onNodeWithContentDescription("Share").performClick() onNodeWithContentDescription(Res.string.transfer_share_title.value).performClick()
runOnIdle { assertEquals(com.vnidrop.app.feature.send.TransferDetailPanel.Share, state.value.detailPanel) } runOnIdle { assertEquals(com.vnidrop.app.feature.send.TransferDetailPanel.Share, state.value.detailPanel) }
waitUntil(timeoutMillis = 5_000) { waitUntil(timeoutMillis = 5_000) {
onAllNodesWithText("Scan with VniDrop to receive this transfer").fetchSemanticsNodes().isNotEmpty() onAllNodesWithText(Res.string.transfer_scan_qr.value).fetchSemanticsNodes().isNotEmpty()
} }
onNodeWithText("Scan with VniDrop to receive this transfer").assertIsDisplayed() onNodeWithText(Res.string.transfer_scan_qr.value).assertIsDisplayed()
onNodeWithText("Save .vnd file").assertIsDisplayed() onNodeWithText(Res.string.button_download_invitation.value).assertIsDisplayed()
onNodeWithContentDescription("Close").assertIsDisplayed() onNodeWithContentDescription(Res.string.button_close.value).assertIsDisplayed()
} }
@Test @Test
@@ -710,12 +750,12 @@ class FoundationComposeTest {
} }
} }
onAllNodesWithText("Share").assertCountEquals(0) onAllNodesWithText(Res.string.transfer_share_title.value).assertCountEquals(0)
onAllNodesWithText("Save .vnd file").assertCountEquals(0) onAllNodesWithText(Res.string.button_download_invitation.value).assertCountEquals(0)
runOnIdle { transfer.value = transfer.value.copy(status = TransferStatus.Failed) } runOnIdle { transfer.value = transfer.value.copy(status = TransferStatus.Failed) }
onAllNodesWithText("Share").assertCountEquals(0) onAllNodesWithText(Res.string.transfer_share_title.value).assertCountEquals(0)
onAllNodesWithText("Save .vnd file").assertCountEquals(0) onAllNodesWithText(Res.string.button_download_invitation.value).assertCountEquals(0)
} }
@Test @Test
@@ -737,8 +777,8 @@ class FoundationComposeTest {
} }
} }
onNodeWithText("QR unavailable for this invitation. Use Share or Download instead.").assertIsDisplayed() onNodeWithText(Res.string.transfer_qr_unavailable.value).assertIsDisplayed()
onNodeWithText("Save .vnd file").assertIsDisplayed() onNodeWithText(Res.string.button_download_invitation.value).assertIsDisplayed()
} }
@Test @Test
@@ -774,10 +814,10 @@ class FoundationComposeTest {
} }
} }
onNodeWithText("Nothing received yet").assertIsDisplayed() onNodeWithText(Res.string.receive_empty_title.value).assertIsDisplayed()
onNodeWithText("Start receiving").performClick() onNodeWithText(Res.string.button_receive_files.value).performClick()
onNodeWithText("How would you like to connect?").assertIsDisplayed() onNodeWithText(Res.string.receive_choose_method_title.value).assertIsDisplayed()
onNodeWithText("Open a .vnd invitation").assertIsDisplayed() onNodeWithText(Res.string.receive_method_file.value).assertIsDisplayed()
} }
@Test @Test
@@ -813,11 +853,11 @@ class FoundationComposeTest {
} }
} }
onNodeWithContentDescription("Delete from receive history").assertIsDisplayed() onNodeWithContentDescription(Res.string.receive_delete_history_item.value).assertIsDisplayed()
onNodeWithText("Clear history").performClick() onNodeWithText(Res.string.receive_clear_history.value).performClick()
onNodeWithText("Clear receive history?").assertIsDisplayed() onNodeWithText(Res.string.receive_clear_history_title.value).assertIsDisplayed()
onNodeWithText("Downloaded files will remain on this device.", substring = true).assertIsDisplayed() onNodeWithText(Res.string.receive_clear_history_description.value).assertIsDisplayed()
onNodeWithContentDescription("Close").assertIsDisplayed() onNodeWithContentDescription(Res.string.button_close.value).assertIsDisplayed()
} }
@Test @Test
@@ -883,3 +923,9 @@ class FoundationComposeTest {
updatedAt = 2L, updatedAt = 2L,
) )
} }
private val StringResource.value: String
get() = runBlocking { getString(this@value) }
private fun StringResource.value(vararg formatArgs: Any): String =
runBlocking { getString(this@value, *formatArgs) }