Pantrix UI

Panoya metin kopyalama

Compose Multiplatform'da bir metni panoya kopyalamanın neden tek bir çağrı olmadığı, bunun için kütüphanenin ne verdiği, bir kopyalama düğmesinin ne zaman ne söylemesi gerektiği ve bunun nasıl sınandığı. Sorun Pantrix'e özgü değildir: ortak kodunda bir "kopyala" düğmesi olan her Compose Multiplatform projesinde aynı şekilde görülür.

Compose hakkında burada söylenen her şey Compose Multiplatform 1.12.1'in kaynaklarında okunmuş ya da bu depoda koşan bir testte görülmüştür; hangisinin nerede görüldüğü 7. ve 8. bölümlerde yazılıdır. Doğrulanamayanlar 8. bölümde ayrıca sayılır.

1. Sorun

Belirti

Neden

ClipEntry ortak kodda yalnızca bir addır: expect class ClipEntry { val clipMetadata: ClipMetadata } (ClipboardManager.kt). Kurucusu ve içindeki her şey platformun kendi bildirimindedir (Compose Multiplatform 1.12.1; Android'de androidx.compose.ui 1.12.1):

PlatformClipEntry orada nedirDüz metin nasıl kurulurİşaret
AndroidBir ClipData'yı saran sınıf: ClipEntry(val clipData: ClipData)ClipEntry(ClipData.newPlainText(etiket, metin))Yok
Masaüstü (JVM)Bir nesneyi saran sınıf: ClipEntry(nativeClipEntry: Any). Pano onu bir java.awt.datatransfer.Transferable olarak okurClipEntry(StringSelection(metin))Kurucu @ExperimentalComposeUiApi
iOSCompose'un kendi sınıfı; kurucusu internalClipEntry.withPlainText(metin)@ExperimentalComposeUiApi
Web, jsClipEntry(clipboardItems: Array<ClipboardItem>)ClipEntry.withPlainText(metin)Kurucu deneysel, withPlainText değil
Web, wasmJsClipEntry(clipboardItems: JsArray<ClipboardItem>)ClipEntry.withPlainText(metin)Kurucu deneysel, withPlainText değil

Kaynak dosyaları: AndroidClipboardManager.android.kt, PlatformClipboard.desktop.kt, PlatformClipboard.ios.kt, PlatformClipboard.js.kt, PlatformClipboard.wasm.kt.

Üç ayrıntı işi büyütür:

  1. İki web hedefi iki ayrı bildirimdir. ClipEntry, js ve wasmJs için ayrı ayrı bildirilmiştir; ikisinin ortak kaynak kümesi (webMain) yalnızca ortak koddaki boş bildirimi görür. Bir expect fonksiyonunun karşılığı bu yüzden beş kez yazılır: Android, iOS, masaüstü, JS, Wasm.

  2. Deneysel işaretler. Masaüstünün kurucusu ve iOS'un withPlainText'i @ExperimentalComposeUiApi taşır; kullanan her yer @OptIn ister.

  3. Yazma askıdadır, çünkü tarayıcıda öyledir. Tarayıcının panosu bir Promise döner; Compose onu bekler ve reddedilirse hata o çağrıdan fırlar. Eski arayüz tarayıcıda yazmayı başlatır, sonucunu beklemezdi (JsPlatformClipboardManager.setText); okuması da orada hep boş dönerdi (getText(): AnnotatedString? = null). Sonucu söyleyemeyen bir arayüzdü.

Platformlar yazmayı nasıl bitirir

PlatformCompose ne yaparNe ters gidebilir
AndroidClipboardManager.setPrimaryClip(clipData)Sistem servisinin hatası çağrıdan bir RuntimeException olarak fırlar (rethrowFromSystemServer, Android SDK kaynağı)
MasaüstüSistemin panosuna setContents(transferable, owner)Pano o an kullanılamıyorsa IllegalStateException (JDK'nın belgesi). Başsız bir JVM'de (java.awt.headless=true) pano yoktur: yazma sessizce atlanır
iOSUIPasteboard.generalPasteboard.string = metinKaynakta bir hata yolu yok
WebTam pano arayüzü varsa navigator.clipboard.write([ClipboardItem]), yoksa writeText(metin); ikisi de beklenirTarayıcı reddederse hata çağrıdan fırlar. Güvenli olmayan bağlamda pano yoktur: yazma sessizce atlanır, konsola bir uyarı düşer

"Sessizce atlanır" denen iki yerde setClipEntry hata fırlatmadan döner: onu saran kod kopyalandığını sanır.

2. Çözüm: üç parça

ParçaNe sağlar
1Türü platformda kur. Tek bir expect fonksiyonu, beş karşılıkOrtak kod "bu metin" der; platformun türünü bilmez
2Sonucu cevap yap. Yazmayı saran askıda bir fonksiyon: kopyalandı mıDüğme olanı söyler, basıldığını değil. Platformun fırlattığı her şey tek bir cevaptır: kopyalanamadı. Tek istisna iptaldir, o yeniden fırlatılır
3Panonun huylarını önceden bil. Yazılacak bir pano var mı; sistem kopyalananı kendisi gösteriyor muHata fırlatmayan iki durum da doğru söylenir: panosu olmayan sayfa ve Android 13

Üçüncü parça olmadan ilk ikisi iki yerde yanılır. Panosu olmayan bir sayfada hata fırlamaz ve cevap "kopyalandı" olur; Android 13'te cevap doğrudur ama uygulama, sistemin az önce gösterdiğini bir kez daha söyler.

3. Pantrix ile

Kütüphane üç parçayı da verir (com.pantrixui.foundation.platform):

APINe yapar
rememberPantrixClipboard()Düz metin için pano: LocalClipboard'ın verdiği panonun üstünde, LocalClipboardTraits'in huylarıyla. Yeniden kurulumlarda aynı nesnedir; pano ya da huylar değişince yenisi verilir
PantrixClipboard.copyText(text, label)Metni panoya koyar ve konup konmadığını söyler (suspend, Boolean). Yazılacak pano yoksa panoya hiç gitmez ve false der. label yalnızca Android'de taşınır
PantrixClipboard.traitsBu panonun huyları
ClipboardTraits(writable, announcedBySystem)İki huy: yazılacak bir pano var mı; sistem kopyalananı kendisi gösteriyor mu
currentClipboardTraits()Platformun huyları, sorulduğu andaki hâliyle. Android'de API 33'ten başlayarak announcedBySystem; tarayıcıda sayfa güvenli bir bağlamda değilse ve masaüstünde JVM başsız çalışıyorsa writable yanlış
LocalClipboardTraitsrememberPantrixClipboard()'un baktığı huylar. Verilmezse platformunkidir; bir test başka bir platformunkini buradan verir

3.1. Kopyalama düğmesi

// commonMain
@Composable
fun CopyLinkButton(link: String) {
    val clipboard = rememberPantrixClipboard()
    val snackbars = LocalPantrixSnackbars.current
    val scope = rememberCoroutineScope()
    PantrixButton(
        text = "Bağlantıyı kopyala",
        onClick = {
            scope.launch {
                // Askıda: tarayıcıda yazma bitince döner. Cevap olanı söyler, basıldığını değil.
                val copied = clipboard.copyText(link, label = "Bağlantı")
                val message = when {
                    !copied -> "Bağlantı kopyalanamadı"
                    // Android 13 ve üstü: sistem kopyalananı kendisi gösterir.
                    clipboard.traits.announcedBySystem -> null
                    else -> "Bağlantı kopyalandı"
                }
                message?.let { snackbars?.show(message = it) }
            }
        }
    )
}

Platforma bakan tek bir satır yoktur: aynı düğme beş hedefte de derlenir. rememberCoroutineScope() düğmenin kendi kapsamıdır; ekran terk edilirse yarım kalan kopyalama da iptal olur ve cevap gelmez.

3.2. Düğme ne der, ne zaman susar

DurumcopyTextHuyDüğme
Kopyalandı; sistem bir şey göstermiyor (masaüstü, iOS, tarayıcı, Android 12L ve altı)trueannouncedBySystem = false"Kopyalandı" der
Kopyalandı; sistem kendisi gösteriyor (Android 13 ve üstü)trueannouncedBySystem = trueSusar
Platform reddetti (tarayıcı izin vermedi, pano o an kullanılamıyor)false—"Kopyalanamadı" der
Yazılacak pano yok (güvenli bağlamda olmayan sayfa)false; panoya gidilmezwritable = false"Kopyalanamadı" der

3.3. Etiket

label panoya konanın ne olduğunu söyler: "Bağlantı", "Kod". Yalnızca Android taşır; ClipData.newPlainText'in belgesi ona "User-visible label for the clip data" der, bu yüzden uygulamanın dilinde verilir. Diğer platformlarda yok sayılır. Verilmezse boştur.

4. Pantrix olmadan

Aynı çözüm, yalnızca Compose Multiplatform'un kendi API'siyle. Aşağıdaki kod herhangi bir projeye kopyalanabilir; kütüphanedeki karşılıklarının sadeleştirilmiş hâlidir.

4.1. Ortak kod (commonMain)

/** [text], bu platformun panosunun aldığı türde. Etiketi yalnızca Android taşır. */
expect fun plainTextClipEntry(label: String, text: String): ClipEntry

/** Panonun bu platformdaki iki huyu. */
data class ClipboardTraits(
    val writable: Boolean = true,
    val announcedBySystem: Boolean = false
)

/** Bu platformun panosunun huyları, sorulduğu andaki hâliyle. */
expect fun currentClipboardTraits(): ClipboardTraits

/** Düğmelerin baktığı huylar. Bir test, başka bir platformun huyunu buradan verir. */
val LocalClipboardTraits = staticCompositionLocalOf { currentClipboardTraits() }

/** [text]'i panoya koyar ve konup konmadığını söyler. */
suspend fun Clipboard.copyText(
    text: String,
    label: String = "",
    traits: ClipboardTraits = currentClipboardTraits()
): Boolean = traits.writable &&
    try {
        setClipEntry(plainTextClipEntry(label, text))
        true
    } catch (cancelled: CancellationException) {
        // İptal bir cevap değildir: yutulursa iptal edilen coroutine yoluna devam eder.
        throw cancelled
    } catch (_: Throwable) {
        // Tarayıcının reddi bir Kotlin `Exception`'ı değildir.
        false
    }

CancellationException burada kotlin.coroutines.cancellation.CancellationException'dır.

4.2. Platform karşılıkları

// androidMain
actual fun plainTextClipEntry(label: String, text: String): ClipEntry =
    ClipEntry(ClipData.newPlainText(label, text))

// Android 13'ten (API 33) başlayarak sistem, panoya konanı kendi önizlemesiyle gösterir.
actual fun currentClipboardTraits(): ClipboardTraits =
    ClipboardTraits(announcedBySystem = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
// jvmMain
@OptIn(ExperimentalComposeUiApi::class)
actual fun plainTextClipEntry(label: String, text: String): ClipEntry = ClipEntry(StringSelection(text))

// Ekranı olmayan (başsız) bir JVM'de sistemin panosu yoktur; Compose oraya yazmayı hata fırlatmadan atlar.
actual fun currentClipboardTraits(): ClipboardTraits =
    ClipboardTraits(writable = !GraphicsEnvironment.isHeadless())
// iosMain
@OptIn(ExperimentalComposeUiApi::class)
actual fun plainTextClipEntry(label: String, text: String): ClipEntry = ClipEntry.withPlainText(text)

actual fun currentClipboardTraits(): ClipboardTraits = ClipboardTraits()
// jsMain ve wasmJsMain: aynı satır, iki ayrı dosyada
actual fun plainTextClipEntry(label: String, text: String): ClipEntry = ClipEntry.withPlainText(text)
// webMain
actual fun currentClipboardTraits(): ClipboardTraits = ClipboardTraits(writable = browserClipboardWrites())

// Tarayıcı panoyu yalnızca güvenli bir bağlamda verir: https, ya da makinenin kendisi.
@OptIn(ExperimentalWasmJsInterop::class)
private fun browserClipboardWrites(): Boolean =
    js("window.isSecureContext === true && Boolean(window.navigator.clipboard && window.navigator.clipboard.writeText)")

Tarayıcıya sorulan soru webMain'de tek kez yazılır; ClipEntry.withPlainText ise yazılamaz, iki hedefte ayrı ayrı durur (bkz. 7. bölüm, "webMain'e yazılan karşılık"). Yalnızca bir web hedefi olan projede ikisi de o hedefin kaynak kümesine yazılır.

4.3. Kullanım

// commonMain
@Composable
fun CopyLinkButton(link: String, snackbars: SnackbarHostState) {
    val clipboard = LocalClipboard.current
    val traits = LocalClipboardTraits.current
    val scope = rememberCoroutineScope()
    Button(
        onClick = {
            scope.launch {
                val copied = clipboard.copyText(link, label = "Bağlantı", traits = traits)
                val message = when {
                    !copied -> "Bağlantı kopyalanamadı"
                    // Android 13 ve üstü: sistem kopyalananı kendisi gösterir.
                    traits.announcedBySystem -> null
                    else -> "Bağlantı kopyalandı"
                }
                message?.let { snackbars.showSnackbar(it) }
            }
        }
    ) {
        Text("Bağlantıyı kopyala")
    }
}

5. Yardımcının yapmadıkları

Yardımcı tek bir iş yapar: düz bir metni panoya koyar ve konup konmadığını söyler. Şunları yapmaz:

İlk ikisini bir proje kendisi yazacaksa yolu yine platformdan platforma ayrılır. Compose'un kaynaklarının gösterdiği kadarı aşağıdadır; okuma sütunu, Compose Foundation'ın metin alanlarında yapıştırırken kullandığı kendi okuyucusundan (ClipboardUtils.*.kt, readText()) alınmıştır. O okuyucu internal'dır, bir uygulama onu çağıramaz.

PlatformMetni geri okumakDüz metinden başkası
AndroidgetClipEntry()?.clipData?.getItemAt(0)?.text?.toString(). Panonun içeriğine ilk erişimde sistem bir uyarı gösterir (Compose'un belgesindeki adıyla "App pasted from Clipboard"); yalnızca panoda ne olduğunu sormak için clipboard.nativeClipboardManager.primaryClipDescriptionHerhangi bir ClipData: ClipEntry(clipData) ya da clipData.toClipEntry(). İçindeki ilk adres için firstUriOrNull() (deneysel)
MasaüstügetClipEntry()?.asAwtTransferable (deneysel); isDataFlavorSupported(DataFlavor.stringFlavor) ise getTransferData(DataFlavor.stringFlavor) as? String. Foundation bunu Dispatchers.IO'da yapar ve IOException'ı "metin yok" sayarHerhangi bir Transferable: ClipEntry(transferable). ClipboardOwner da ise panonun sahibi olarak verilir
iOSgetClipEntry()?.getPlainText() (deneysel). Pano boşsa getClipEntry() null dönerCompose'un ClipEntry'si yalnızca metin taşır ("Currently, it operates only with string(s)"); gerisi için UIPasteboard doğrudan kullanılır
WebgetClipEntry()?.clipboardItems (deneysel); ilk öğenin getType("text/plain")'i bir Blob verir, metni blob.text() ile, bir Promise olarak gelir. Dizi js'te Array, wasmJs'te JsArray'dir: iki hedefte ayrı yazılır. Tarayıcı okumayı reddederse Compose hata fırlatmaz, içi boş bir giriş döner; read hiç yoksa null dönerKendi ClipboardItem'larınla ClipEntry(clipboardItems) (kurucu deneysel). Tam pano arayüzü olmayan tarayıcıda Compose yalnızca writeText ile düz metni yazar

6. Test etmek

6.1. Sahte pano

Compose bir teste gerçek panoyu verir (bkz. 7. bölüm, "Sahtesi verilmeyen pano"). Kopyalayan her test kendi panosunu LocalClipboard ile verir:

/** Konan her şeyi tutan, makinenin panosuna dokunmayan bir pano. */
class RecordingClipboard(private val refuses: Boolean = false) : Clipboard {
    val entries = mutableListOf<ClipEntry?>()

    override suspend fun getClipEntry(): ClipEntry? = entries.lastOrNull()

    override suspend fun setClipEntry(clipEntry: ClipEntry?) {
        check(!refuses) { "NotAllowedError: Write permission denied." }
        entries += clipEntry
    }
}

Clipboard ortak koddadır: bu sahte, ortak bir testte de yazılır. Orada bakılabilenler kaç kez yazıldığı ve copyText'in ne dediğidir; gelen ClipEntry'nin içindeki metin ortak kodda okunamaz.

6.2. Başka bir platformun huyu

Android 13'ün ya da panosu olmayan bir sayfanın davranışı, o platformda koşmayan bir testte de sınanır: huylar LocalClipboardTraits ile verilir.

// jvmTest
@OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
class CopyLinkButtonTest {
    @Test
    fun a_copy_the_system_shows_itself_is_not_told_twice() = runComposeUiTest {
        val clipboard = RecordingClipboard()
        val snackbars = PantrixSnackbarState()
        setContent {
            PantrixTheme {
                CompositionLocalProvider(
                    LocalClipboard provides clipboard,
                    // Android 13'ün huyu, masaüstünde koşan bir testte.
                    LocalClipboardTraits provides ClipboardTraits(announcedBySystem = true),
                    LocalPantrixSnackbars provides snackbars
                ) {
                    CopyLinkButton(link = "https://example.com")
                }
            }
        }
        onNodeWithText("Bağlantıyı kopyala").performClick()
        waitForIdle()
        // Metin masaüstünün okuduğu gibi okunur: `Transferable`'ın metin biçimiyle.
        val copied = clipboard.entries.single()?.asAwtTransferable?.getTransferData(DataFlavor.stringFlavor)
        assertEquals("https://example.com", copied)
        assertEquals(null, snackbars.current)
    }
}

ClipboardTraits(writable = false) ile aynı test panosu olmayan sayfayı sınar: sahte panoya hiçbir şey gelmez ve düğme "kopyalanamadı" der. RecordingClipboard(refuses = true) reddeden bir tarayıcıdır.

6.3. Gelen metni okumak

Metnin kendisine her platformun testi kendi yoluyla bakar:

PlatformTest nerede koşarMetin nasıl okunur
MasaüstüjvmTest, sahte panoylaentry.asAwtTransferable?.getTransferData(DataFlavor.stringFlavor)
AndroidRobolectric, gerçek panoyla: Robolectric'in panosu testin belleğindedirLocalClipboard.current.nativeClipboardManager.primaryClip: ilk öğenin text'i, description.label
iOSSimülatör, sahte panoylaentry.getPlainText()
WebTarayıcı, Compose'un kendi panosuylanavigator.clipboard.write ve writeText yerine testin fonksiyonları konur; write'a gelen öğenin text/plain parçası okunur

Web'de yol başkadır. webTest, webMain gibi iki hedefin ortak kaynak kümesidir ve ClipEntry'nin içi orada tek bir biçimde yazılamaz: öğeleri bir hedefte Array, ötekinde JsArray'dir. Testin yerine geçtiği şey Compose'un panosu değil, tarayıcınınkidir: böylece Compose'un tarayıcıya gerçekten ne verdiği görülür, reddedilen bir yazma da gerçek yolundan geçer (Promise.reject(new DOMException(…, 'NotAllowedError'))). İki fonksiyonun da yerine geçilir; hangisinin çağrılacağına Compose tarayıcıya bakarak karar verir.

7. Tuzaklar

TuzakNe olurNe yapılırNerede görüldü
Sahtesi verilmeyen panoTestte LocalClipboard platformun gerçek panosudur: masaüstünde sistemin panosu. Kopyalayan bir test, makinede o an kopyalanmış ne varsa üstüne yazarHer testte kendi Clipboard'unu verKaynak: RootNodeOwner.skiko.kt (createPlatformClipboard())
Ortak kodda metni okumakSahte panoya gelen ClipEntry'nin ortak koddaki tek üyesi clipMetadata'dır; metne ulaşılamazOrtak testte yazmaları say; metni platformun kendi testinde oku (6.3)Kaynak; bu testler yazılırken
clipMetadataMasaüstünde, iOS'ta ve web'de TODO()'dur: çağrılınca NotImplementedError fırlarÇağırmaKaynak: dört PlatformClipboard.*.kt (CMP-1260)
Güvenli olmayan bağlamDüz http ile sunulan sayfada pano yoktur. Compose yazmayı atlar, konsola bir uyarı yazar, hata fırlatmaz: sonucu hatadan anlayan düğme "kopyalandı" derYazmadan önce sor: traits.writableCompose'un atlaması kaynakta: PlatformClipboard.js.kt, .wasm.kt. writable'ın "yok" demesi testte, navigator.clipboard gizlenerek
Başsız JVMjava.awt.headless=true ile sistemin panosu yoktur. Compose yazmayı atlar, hata fırlatmaz: sonucu hatadan anlayan düğme "kopyalandı" derYazmadan önce sor: masaüstünde writable, !GraphicsEnvironment.isHeadless()'tirKaynak: PlatformClipboard.desktop.kt. Huy eklenmeden önce geçici bir testte: cevap true, geri okunan null. Huyun kuralı testte: desktopClipboardTraits(headless = true)
catch (e: Exception)Tarayıcının reddi bir Kotlin Exception'ı değildir (JS'te tarayıcının kendi hatası, Wasm'da JsException). Yalnızca Exception yakalayan kod onu kaçırır; hata düğmenin coroutine'inden fırlarThrowable yakalaTest: yakalama Exception'a daraltılınca a_write_the_browser_refuses_is_answered_false JS'te de Wasm'da da kırmızı
Yutulan iptalcatch (e: Throwable) iptali de yakalar: terk edilen bir ekranın coroutine'i "kopyalanamadı" cevabıyla yoluna devam ederÖnce CancellationException'ı yakala ve yeniden fırlatTest: a_copy_whose_coroutine_is_cancelled_is_cancelled_and_answers_nothing
İki kez söylenen kopyaAndroid 13 ve üstünde sistem kopyalananı kendi önizlemesiyle gösterir; uygulama da söylerse aynı şey iki kez söylenirannouncedBySystem iken sus; kopyalanamayanı yine söyleAndroid'in belgeleri; bkz. 8. bölüm, "Doğrulanmayanlar"
webMain'e yazılan karşılıkClipEntry.withPlainText webMain'de çağrılırsa iki hedefin derlemesi geçer; ortak kaynak kümesinin derlemesi (compileWebMainKotlinMetadata) ve editör "Unresolved reference 'withPlainText'" derKarşılığı jsMain ve wasmJsMain'e ayrı ayrı yazGeçici bir dosyayla derlenerek
Deneysel işaretlerMasaüstünün kurucusu, iOS'un withPlainText'i ve okuma tarafındaki asAwtTransferable, getPlainText(), clipboardItems @ExperimentalComposeUiApi taşır; Compose'un sonraki bir sürümünde değişebilir@OptIn tek bir yerde, yardımcının içinde dursunKaynak
Tek fonksiyonun yerine geçen web testiCompose tam pano arayüzü varsa write'ı, yoksa writeText'i çağırır. Test yalnızca birinin yerine geçmişse öteki gerçek panoya yazarİkisinin de yerine geç; kopyalamadan önce yerlerinde olduklarına bakKaynak: PlatformClipboard.js.kt, .wasm.kt
Sahte panoda nativeClipboardArayüzdeki nativeClipboard kullanımdan kalkmıştır ve verilmezse NotImplementedError fırlatır. Masaüstünde metin alanının sağ tık menüsü panoyu hâlâ onunla sorarKopyalama düğmesi için gerekmez. Aynı testte bir metin alanının menüsü de varsa sahte panoda onu da verKaynak: Clipboard.kt, ClipboardUtils.desktop.kt. Bu depoda TextFieldDesktopTest
Android'de okumakgetClipEntry() panonun içeriğine erişir; ilk erişimde sistem "App pasted from Clipboard" uyarısını gösterirYalnızca ne olduğunu sormak için primaryClipDescriptionKaynak: Clipboard.kt'nin belgesi
Dil taşımayan etiketEtiket kullanıcıya görünür bir metindir; koda gömülen bir etiket uygulamanın dili değişince eskisinde kalırEtiketi çağıran versin, uygulamanın dilindeAndroid SDK kaynağı (ClipData.newPlainText); test: etiket Android'de gelir, başka yerde gelmez

8. Doğrulama

Testler

TestNerede koşarNeye bakar
ui/foundation, PantrixClipboardTestJVM, iOS simülatörü, JS, Wasm (9 test)Kopyalama panoya bir giriş koyar ve true der; yazılacak pano yokken false der ve panoya hiç dokunmaz; pano ne fırlatırsa fırlatsın cevap false'tur; iptal edilen kopyalama iptal olur, cevap vermez; hatırlanan pano yeniden kurulumda aynıdır, pano ya da huylar değişince yenisidir ve yenisine bakar
ui/foundation, PantrixClipboardOnTheDesktopTestJVM (3 test)Gelen şey, metin biçimi verilen metin olan bir Transferable'dır; satır sonları, ASCII dışı harfler ve emoji eksiksiz gelir; masaüstünün huyları: ekranı olan JVM'de yazılır, başsız olanda yazılmaz
ui/foundation, PantrixClipboardOnAndroidTestRobolectric (7 test)Sistemin panosundaki ilk öğe verilen metindir, etiketi verilen etikettir, türü düz metindir; etiket verilmezse boştur; announcedBySystem API 33 ve 34'te doğru, 32 ve 30'da yanlıştır
ui/foundation, PantrixClipboardOnIosTestiOS simülatörü (3 test)Gelen girişin getPlainText()'i verilen metindir; zor metin eksiksiz gelir; iOS'un huyları
ui/foundation, PantrixClipboardOnTheWebTestChromeHeadless'ta JS ve Wasm (5 test)Test koşucusunun sayfasında writable doğrudur; navigator.clipboard gizlenince yanlıştır; tarayıcıya verilen öğenin text/plain parçası verilen metindir; zor metin eksiksiz gelir; tarayıcının reddettiği yazmanın cevabı false'tur
app/shared, CodeBlockTestJVM (5 test)Örnek uygulamanın kopyalama düğmesi kodu panoya koyar ve söyler; reddeden panoyu, panosu olmayan sayfayı "kopyalanamadı" diye söyler; sistemin gösterdiği kopyayı söylemez
app/shared, CopyMessageTestJVM, iOS simülatörü, JS, Wasm (3 test)Kopyalamadan sonra ne dendiğinin kuralı

Android'deki 32 için ayrı bir çalışma zamanı indirilmedi: Robolectric sürüm numarasını yazılabilir bir alanda tutar, test 33'ün çalışma zamanında o alana 32 yazar ve geri koyar. 30, 33 ve 34 kendi çalışma zamanlarında koşar.

Elle bozulan davranışlar

Her davranış bir kez elle bozuldu ve hangi testin kırmızı olduğuna bakıldı; sonra geri alındı.

Doğrulanmayanlar

9. Bu depoda nerede

DosyaNe
ui/foundation/src/commonMain/kotlin/com/pantrixui/foundation/platform/PantrixClipboard.ktClipboardTraits, currentClipboardTraits, LocalClipboardTraits, PantrixClipboard, rememberPantrixClipboard
ui/foundation/src/androidMain/…/PantrixClipboard.android.kt, iosMain/…/PantrixClipboard.ios.kt, jvmMain/…/PantrixClipboard.jvm.ktÜç platformun huyları ve panoya konan türü
ui/foundation/src/webMain/…/PantrixClipboard.web.ktTarayıcının huyları ve tarayıcıya sorulan soru
ui/foundation/src/jsMain/…/PantrixClipboard.js.kt, wasmJsMain/…/PantrixClipboard.wasmJs.ktİki web hedefinde panoya konan tür
ui/foundation/src/commonTest/…/platform/RecordingClipboard.ktTestlerin sahte panosu
ui/foundation/src/commonTest, jvmTest, androidHostTest, iosTest, webTest altında …/platform/PantrixClipboard*Test.kt
app/shared/src/commonMain/kotlin/com/pantrixui/cmp/demo/page/CodeBlock.ktÖrnek uygulamanın kopyalama düğmesi ve ne diyeceğinin kuralı (copyMessage)

10. Açık kalanlar