Pantrix UI

Çalışma anı tanılaması

Bazı kusurların neden yalnızca çalışan bir ekranda göründüğü, bunları yakalamak için kütüphanenin ne verdiği, bir uygulamanın tanılamayı yalnızca kendi debug derlemesinde nasıl açacağı ve bunun nasıl sınandığı. Sorun Pantrix'e özgü değildir: otomasyon etiketi, çalışma anında seçilen renkler ya da dışarıdan gelen metinler kullanan her Compose Multiplatform projesinde aynı şekilde görülür.

Compose hakkında burada söylenen her şey Compose Multiplatform 1.12.2'nin kaynaklarında okunmuş ya da bu depoda koşan bir testte görülmüştür. Kotlin ve Gradle eklentileri hakkında söylenenler Kotlin 2.4.21'in ve Compose Gradle eklentisi 1.12.2'nin kaynaklarında okunmuştur. 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

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

ParçaNe sağlar
1Denetim bileşenin içinde. Her bileşen ne çizdiğini, adını, etiketini ve kısayolunu bildiği yerde söylerÖlçülen, çalışma anındaki değerdir: verilen renk, gelen ad, ekrandaki düğümler
2Kapalıyken bedava. Denetimler ilk satırlarında dönerYayın derlemesi neredeyse hiçbir şey ödemez; açıp kapamak için ekran yeniden kurulmaz
3Alıcı. Bulunan bir yere verilir: günlüğe, bir toplayıcıya, bir testeAynı denetim testte de, debug derlemesinde de çalışır
4Zamanlama. Yerleşimin bulduğu yarım saniye, yinelenen bir etiket bir buçuk saniye sürerse bildirilir; her bulgu sürdüğü sürece bir kezGeçişler ve açılan satırlar yanlış alarm vermez; günlük aynı bulguyla dolmaz

Birinci parça olmadan ikincisi anlamsızdır: bileşenin dışından bakan bir denetim, bir düğmenin rengini ya da adını bilmez. Üçüncü parça olmadan ilk ikisi yalnızca bir testte işe yarar; dördüncüsü olmadan bir geçiş her seferinde bulgu üretir.

3. Pantrix ile

Kütüphane dört parçayı da verir (com.pantrixui.foundation.diagnostics; inspector'ı com.pantrixui.components.overlay.inspector):

APINe yapar
PantrixTheme(diagnostics = …)Temanın tanılaması: içindeki bileşenlerin denetimleri bulduklarını buraya verir. Varsayılanı PantrixDiagnostics.Off'tur. İçteki bir tema verilmezse dıştakinin alıcısını kullanır
PantrixDiagnostics.OffDenetimleri kapatır: hiçbir şey aranmaz, hiçbir şey bildirilmez
PantrixDiagnostics.LogHer bulguyu platformun günlüğüne yazar: Android'de Logcat'e Pantrix etiketiyle bir uyarı (Log.w), iOS'ta birleşik günlüğe (NSLog, Xcode'un konsolu), tarayıcıda console.warn, JVM'de standart hata
PantrixFindings(then = …)Bulguları saklar (all, clear(), boundsOf(finding)), her birini then'e de verir. all bir kompozisyonda okunursa büyüdükçe izlenir
PantrixFindingBir bulgu: check, component, testTag, screen (TestScope zinciri), message (İngilizce) ve ölçülen values
PantrixCheckAltı denetim (3.1)
LocalPantrixDiagnosticsO noktadaki alıcı
PantrixDiagnosticsInspector(findings) { … }Uygulamanın üstünde bulguların sayısını gösteren bir düğme; basınca bulguların listesi; listeden seçilen bulgunun düğümü ekrana bir sonraki basışa kadar çerçevelenir. Bulgu yokken bir şey göstermez
Modifier.checkName(component, name)Uygulamanın kendi bileşeni için MissingName
Modifier.checkContrast(component) { pairs }, ContrastPair, ContrastNeedUygulamanın kendi bileşeni için LowContrast
contrastRatio(foreground, background)İki rengin WCAG 2.2 oranı

3.1. Altı denetim

DenetimNe zaman bildirilirNereden gelirNasıl düzelir
DuplicateTestTagAynı penceredeki iki düğüm aynı etiketi bir buçuk saniye taşırsa: bir sayfa geçişinden uzunModifier.pantrixTestTag: bütün Pantrix bileşenleri ve onu kullanan uygulama koduHer birine kendi tagKey'i, ya da her birini kendi TestScope'una koymak
LowContrastTemanın 34 rol çifti, renkleri her değiştiğinde bir kez; bileşenlerin yazı ve işaret çiftleri, bileşen ve renkleri için bir kez, kaç tanesi gösterirse göstersinPantrixTheme; kendi …Colors sınıfı olan 39 bileşen; checkContrastYazı 4,5:1; büyük yazı ve bir kontrolün işareti 3:1
SmallTouchTargetBir kontrol platformun dokunma hedefinden az yere yarım saniye yerleşirseModifier.minTouchTarget: Pantrix'in basılan kontrolleriÜstündekine yer verdirmek
MissingNameEkran okuyucunun söyleyeceği ad boşsa. null bildirilmez: süs olan bir resim ya da yazısıyla adlanan bir kontrol adını vermezcontentDescription, label ya da title alan bileşenler; checkNameUygulamanın dilinde, ne olduğunu ya da ne yaptığını söyleyen bir ad
NoStringsForLanguageTemanın dili İngilizce değilken sözleri İngilizceysePantrixThemePantrixTheme(strings = …) ile o dilin sözleri
ShadowedShortcutBir kısayolu çevresindeki bir düğüm de dinliyorsa: içteki hiç çalışmazModifier.onShortcutİçtekini kaldırmak ya da başka bir tuşa almak

Temanın 34 çifti: altı rolün (primary, success, info, warning, danger, neutral) her biri için onColor color üstünde, onContainer container üstünde, color background ve surface üstünde; sayfanın onSurface, textPrimary ve textSecondary yazıları ile borderStrong ve focusRing işaretleri, background ve surface üstünde.

Dolgusuz zemin. Dolgusu olmayan bir düğmenin zemini gibi, altındakini gösteren bir zemin, bileşen temanın kendi zeminlerinden birinde duruyorsa temanın background'u ve surface'ı üstünde ölçülür ve düşük olanı sayılır. Bileşenin durduğu yer kendi içerik rengini veren başka renkte bir dolguysa (ProvidePantrixContentColor, koyu bir kartın yaptığı gibi) böyle bir çift ölçülmez: altında ne olduğu bilinmez.

3.2. Yalnızca debug derlemesinde

Pantrix hangi tür bir derlemede çalıştığını bilmez; bunu uygulama söyler. En sade yol, her platformun giriş noktasının ortak koda bir debug vermesidir:

// commonMain
@Composable
fun App(debug: Boolean) {
    val findings = remember { PantrixFindings(then = PantrixDiagnostics.Log) }
    PantrixTheme(diagnostics = if (debug) findings else PantrixDiagnostics.Off) {
        if (debug) PantrixDiagnosticsInspector(findings) { Screens() } else Screens()
    }
}

debug bir derlemenin ömrü boyunca değişmez; bu yüzden if burada güvenlidir. Çalışırken açılıp kapanan bir ayar için 7. bölümdeki ilk tuzağa bakın.

Her platform debug derlemesini kendi yolundan bilir:

PlatformNereden bilinirGiriş noktasında
AndroidUygulamanın debuggable bayrağından: debug derleme türü onu açarval debug = (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0, Activity'nin onCreate'inde. Android Gradle eklentisi 8'den beri BuildConfig kendiliğinden üretilmez (buildFeatures { buildConfig = true }); bu bayrak ona bağlı değildir. Paylaşılan kod bir kütüphane modülündeyse bayrağı uygulama modülü okur ve ona verir
iOSKotlin/Native ikilisinin derleme türünden: Xcode'un Debug yapılandırması debug framework'ünü derletir@OptIn(ExperimentalNativeApi::class) val debug = Platform.isDebugBinary (kotlin.native.Platform), MainViewController'da. embedAndSignAppleFrameworkForXcode türü Xcode'un CONFIGURATION'ından okur; Debug ve Release dışındaki bir yapılandırmada KOTLIN_FRAMEWORK_BUILD_TYPE'tan
MasaüstüJVM'nin bir derleme türü yoktur: geliştirirken koşan run görevi bir sistem özelliği verirbuild.gradle.kts: tasks.named<JavaExec>("run") { systemProperty("app.debug", "true") }; main'de System.getProperty("app.debug") == "true". Paketlenen uygulama (packageDmg, packageMsi) run ile başlamaz ve özelliği almaz
WebTarayıcıdaki kodun okuyabileceği bir derleme türü yokturSayfanın adresi: window.location.search.contains("debug") (kotlinx.browser). Adresi bilen herkes açabilir; tanılama yalnızca konsola yazdığı için bu çoğu zaman yeter. Yetmezse derleme bir sabit yazar

3.3. Bir testte

@Test
fun the_checkout_screen_has_no_findings() = runComposeUiTest {
    val findings = PantrixFindings()
    setContent { PantrixTheme(diagnostics = findings) { CheckoutScreen() } }
    // Yerleşimin bulduğu yarım saniye, yinelenen bir etiket bir buçuk saniye sürünce bildirilir.
    mainClock.advanceTimeBy(2_000)
    assertEquals(emptyList(), findings.all)
}

Bir bulgunun message'ı testin hata iletisinde yeter: neyin yanlış olduğunu ve nasıl düzeleceğini İngilizce söyler. values ölçüleni taşır (LowContrast'ta ratio, needed, foreground, background ve ölçüldüyse ground; SmallTouchTarget'ta width, height, needed).

3.4. Kendi bileşenin

Uygulamanın kendi bileşeni de denetimlere katılır. Adı ve renkleri bileşenin kendisi bilir:

@Composable
fun Swatch(name: String, fill: Color, label: Color) {
    Column(
        modifier = Modifier
            .checkName("Swatch", name)
            .checkContrast("Swatch") { listOf(ContrastPair("label on fill", label, fill, ContrastNeed.Text)) }
            .background(fill)
            .padding(12.dp)
    ) {
        PantrixText(text = name, color = label)
    }
}

checkContrast'ın çiftleri yalnızca tanılama açıkken istenir; tanılama ya da temanın renkleri değişince yeniden. Devre dışı bir bileşen, kimsenin okuması beklenmediği için çift vermez.

3.5. Ayrı bir alıcısı olan bölüm

İçteki bir tema kendi alıcısını alabilir: PantrixTheme(diagnostics = own) { … }. O bölümün bulguları yalnızca own'a gider, dıştakine gitmez. Bu depodaki demo, "Tanılama" sayfasının örneklerini böyle gösterir: her örneğin kusuru kendi alıcısında toplanır ve uygulamanın kendi inspector'ına düşmez.

4. Pantrix olmadan

Bileşenin içinden yapılan denetimler (renk, ad, dokunma hedefi) bir kütüphane ya da uygulamanın kendi bileşen katmanı ister: dışarıdan bakan bir kod, bir düğmenin hangi rengi hangisinin üstüne çizdiğini bilmez. Aşağıdaki iki parça ise her projeye olduğu gibi kopyalanabilir.

4.1. Kontrast oranı (commonMain)

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.luminance

/** WCAG 2.2: yazı 4.5 ve üstünde, büyük yazı ve bir kontrolün işareti 3 ve üstünde okunur. */
fun contrastRatio(foreground: Color, background: Color): Float {
    require(foreground.isSpecified && background.isSpecified) { "An unspecified colour has no contrast." }
    val drawn = if (foreground.alpha < 1f) foreground.compositeOver(background) else foreground
    val one = drawn.luminance() + 0.05f
    val other = background.luminance() + 0.05f
    return maxOf(one, other) / minOf(one, other)
}

Bu, kütüphanenin contrastRatio'sunun gövdesidir. Arkasını gösteren bir ön renk çizildiği gibi, zeminin üstüne konarak ölçülür; zemin verildiği gibi ölçülür.

4.2. Bir testte tekrar eden etiketler (commonTest)

import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider

/** İki ya da daha çok düğümün taşıdığı etiketler ve her birini kaç düğümün taşıdığı. */
fun SemanticsNodeInteractionsProvider.repeatedTestTags(): Map<String, Int> =
    onAllNodes(SemanticsMatcher.keyIsDefined(SemanticsProperties.TestTag), useUnmergedTree = true)
        .fetchSemanticsNodes()
        .groupingBy { it.config[SemanticsProperties.TestTag] }
        .eachCount()
        .filterValues { it > 1 }

Bir ekran testinin sonunda assertEquals(emptyMap(), repeatedTestTags()). Test bulucularının saydığını sayar: yerleşmemiş düğümleri de, geri dönüştürülen satırları değil. Testin baktığı an tek bir andır; bir geçişin ortasında bakarsa geçici bir tekrarı da bulur.

5. Denetimlerin yapmadıkları

6. Test etmek

7. Tuzaklar

TuzakNe olurNe yapılırNerede görüldü
Bir koşulla sarılan inspectorif (on) PantrixDiagnosticsInspector(findings) { content() } else content(), on çalışırken değişirse içeriği kompozisyonda başka bir yere taşır. İçerikteki her remember ve rememberSaveable baştan kurulur: demo'da "Ayarlar"da tanılamayı kapatmak uygulamayı genel bakışa döndürüyorduInspector'ı hep yerinde bırak: tanılama kapalıyken bulgu yoktur ve inspector bir şey göstermezDemo'yu yazarken; test: DemoShellTest.the_diagnostics_switch_leaves_the_app_where_it_is (koşul geri konunca kırmızı)
Kapanınca kalan bulgularAlıcı Off'a dönünce yeni bulgu gelmez, ama PantrixFindings eskilerini saklar: inspector eski sayıyı gösterirTanılama kapanınca clear()Test: SettingsPageTest.turned_off_the_diagnostics_forget_what_they_found
Bir sayfanın örnekleriBir katalog sayfasındaki örnekler aynı anda ekrandadır: iki örnekte aynı anahtar, gerçek bir tekrardırÖrneklerin anahtarlarını ayır; bir yardımcıyı iki örnek kullanıyorsa ona bir anahtar verDemo: beş sayfada on dört tekrar bulundu ve düzeltildi
Bir geçişin iki sayfasıGeçiş, sürdüğü boyunca iki sayfayı birlikte bağlı tutar: Navigation 3'ün varsayılanlarıyla Android'de 700 ms, iOS'ta 500 ms. İkisinde aynı etiket varsa her karede bakan bir denetim bunu tekrar sayar; yarım saniye bekleyen de sayıyordu (0.1.0-alpha01'in sürüm APK'sında ve iOS uygulamasında görüldü)Bir buçuk saniye bekleyen denetim bunu bildirmezKaynak: SemanticsOwner, Navigation 3'ün NavDisplay'i; test: DuplicateTestTagTest.a_tag_two_pages_carry_while_one_gives_way_to_the_other_is_not_reported
Geri dönüştürülen satırlarKaydırılan bir listenin devre dışı bıraktığı satırların değiştiricileri ağaçtan ayrılır; yeniden kullanılınca bağlanırBir şey yapılmaz: sayılmazlarKaynak; test: DuplicateTestTagTest.the_rows_of_a_list_that_scrolls_and_reuses_them_are_no_duplicates
Dolgulu zeminde dolgusuz bileşenBeyaz yazılı, dolgusuz bir düğme koyu bir kartın üstünde okunur; sayfanın açık zemininde ölçülseydi okunmaz sayılırdıDolgunun içerik rengini ver (ProvidePantrixContentColor). Pantrix'in yüzeyi ve kartı bunu kendileri yapar: başka renkte bir dolguda onun içerik rengini verirler, temanın zemininde hiçbirini. İçerik rengi verilen yerde çift ölçülmezTest: ComponentDiagnosticsTest.a_button_with_no_fill_on_a_fill_of_another_colour_is_not_measured_on_the_page
İçteki temanın alıcısıİçteki bir tema diagnostics alırsa bulguları dıştakine gitmezBütün uygulamanın bulguları için içteki temalara alıcı vermeTest: PantrixDiagnosticsTest.a_theme_inside_another_reports_where_that_one_does_unless_it_is_told_otherwise
Kendi alıcısı olan bölümlerin tekrarlarıKendi alıcısı olan iki bölüm aynı etiketi taşırsa tekrar o bölümlerin alıcılarına bildirilir, dıştakine değil: uygulamanın kendi bulgularına bakan bir tarama onu görmez. Demo'nun "Diagnostics Inspector" sayfasında iki örnek aynı ekranı aynı anahtarlarla gösteriyordu ve her inspector iki yerine dört bulgu sayıyorduKendi alıcısı olan bölümlerin bulgularına da bak; aynı yardımcıyı kullanan iki bölümün anahtarlarını ayırWeb'de demo'ya bakarken; test: DiagnosticsSamplesTest.each_inspector_counts_the_two_findings_of_its_own_screen
Durum listesinin eşitliğiCompose'un SnapshotStateList'i yapısal eşitlik tanımaz. assertEquals(emptyList(), list) JVM'de geçer, çünkü JUnit expected.equals(actual) sorar ve boş liste her boş listeye eşittir; iOS'ta ve web'de kotlin.test actual == expected sorar ve iki boş liste eşit çıkmazDışarıya bir durum listesi verilecekse okunduğu andaki kopyası verilir: PantrixFindings.all öylediriOS ve JS test koşusunda: all listenin kendisini verirken 21 test kırmızıydı
NSLog'un değişken argümanlarıKotlin/Native bir Kotlin metnini NSLog'un ... argümanına C dizgisi olarak verir; NSLog("%@", line) onu bir nesne sanar ve uygulama EXC_BAD_ACCESS ile çökerSatırı biçimin kendisi yap, yüzde işaretlerini ikile: NSLog(line.replace("%", "%%"))iOS simülatöründe demo'nun "Tanılama" sayfası açılınca; test: PantrixDiagnosticsOnIosTest (eski hâlinde test süreci signal 11 ile düşer)
Elle ilerleyen saatmainClock.autoAdvance = false bir testte yerleşimin bulguları hiç gelmezSaati en az 500 ms, yinelenen etiket için en az 1500 ms ilerletTestler bu depoda böyle yazıldı
Uygulamanın kendi kısayoluUygulamanın kökü bir kısayol dinliyorsa (demo'da ⌘K komut paletini açar), aynı kısayolu dinleyen her ekran onun gölgesinde kalır. Demo'nun "Gölgede kalan kısayol" örneği ⌘K dinlediği için ikinci bir bulgu gösteriyordu: ekranın kökü de uygulamanınkinin gölgesindeydiEkranları testte uygulamanın kökünün altında çiz; örnekte uygulamanın kullanmadığı bir tuş (⌘F)Web'de demo'ya bakarken; test: DiagnosticsSamplesTest
Masaüstünün hedefiFareyle kullanılan masaüstünde ve web'de dokunma hedefi 32dp'dir: 32dp'lik bir kutu orada sıkıştırmaz, telefonda sıkıştırır. Demo'nun örneği masaüstünde ve web'de bulgu göstermiyorduBir sıkışmayı her platformda göstermek için hedeflerin en küçüğünden alçak bir kutu (24dp)Web'de demo'ya bakarken; test: DiagnosticsSamplesTest
Günlüğün yeriAndroid'de bulgular Pantrix etiketiyle Logcat'tedir; iOS'ta Xcode'un konsolunda; tarayıcıda konsolda bir uyarı olarak; masaüstünde standart hatadaAndroid'de adb logcat -s PantrixKaynak: dört PantrixDiagnostics.*.kt

8. Doğrulama

Testler

TestNerede koşarNeye bakar
ui/foundation, PantrixDiagnosticsTestJVM, iOS simülatörü, JS, WasmTema alıcı verilmedikçe hiçbir şey aramaz; içteki tema dıştakinin alıcısını kullanır; bulgular bir kez ve geldikleri sırayla saklanır; şüphe bir sonraki karede, yerleşiminki yarım saniye, yinelenen etiket bir buçuk saniye sonra bildirilir; süresi dolmadan kalkan bildirilmez; bir kez bildirilir, geri gelirse yeniden; aynı şüphenin sahipleri tek şüphedir; masa yalnızca bekleyen bir şey varken kare ister; günlük satırı
ui/foundation, PantrixDiagnosticsOnIosTestiOS simülatörüYüzde işaretleri ve %@ taşıyan bir bulgu birleşik günlüğe yazılır ve uygulama çökmez
ui/foundation, ContrastRatioTestAynı dört hedefBeyaz üstünde siyah 21, kendi üstünde bir renk 1; hangisinin üstte çizildiği fark etmez; arkasını gösteren ön renk çizildiği gibi ölçülür; belirtilmemiş bir rengin oranı yoktur
ui/foundation, DuplicateTestTagTest, SmallTouchTargetTest, LowContrastTest, MissingNameTest, NoStringsForLanguageTest, ShadowedShortcutTestAynı dört hedefHer denetim: bulduğu, bulmadığı, kapalıyken hiçbir şey ölçmediği, sonradan açılınca bulduğu
ui/components, ComponentDiagnosticsTest, DiagnosticsInspectorTestJVM, iOS simülatörü, JS, WasmPantrix'in kendi renkleri iki modda da bulgu değildir; bir bileşenin bulgusu etiketiyle gelir; dolgulu zeminde dolgusuz bileşen; adlar, tekrarlar, sıkışan hedef; inspector'ın düğmesi, listesi, çerçevesi ve temizlemesi
ui/components, ComponentScreenshotTest.diagnostics_inspectorJVM, Android, iOS simülatörüInspector'ın açık ve koyu temadaki görüntüsü
app/shared, DemoPagesTest, DiagnosticsSamplesTest, DemoShellTest, SettingsPageTest, DemoSettingsTestJVMHer sayfa iki dilde sıfır bulguyla; iki tanılama sayfasının her örneği yalnızca kendi bulgularını gösterir; ayar açılıp kapanınca uygulama yerinde kalır; kapanınca bulgular silinir; ayar saklanır ve eski kayıtta açıktır

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/diagnostics/PantrixDiagnostics.ktPantrixCheck, PantrixFinding, PantrixDiagnostics, LocalPantrixDiagnostics
…/diagnostics/PantrixFindings.ktPantrixFindings
…/diagnostics/DiagnosticsDesk.ktPencere başına masa: şüpheler, yarım saniye, bir kez bildirme
…/diagnostics/DuplicateTestTag.kt, LowContrast.kt, MissingName.kt, NoStringsForLanguage.ktDenetimler; checkContrast, ContrastPair, ContrastNeed, checkName
…/a11y/TouchTarget.kt, …/keyboard/Shortcuts.kt, …/testing/TestTag.ktSmallTouchTarget, ShadowedShortcut ve etiketlerin kaydı
…/token/color/ContrastRatio.ktcontrastRatio
ui/foundation/src/{android,ios,jvm,web}Main/…/diagnostics/PantrixDiagnostics.*.ktPlatformların günlükleri
ui/components/src/commonMain/kotlin/com/pantrixui/components/overlay/inspector/DiagnosticsInspector.ktPantrixDiagnosticsInspector
ui/components/src/commonMain/kotlin/com/pantrixui/components/chart/common/ChartContrast.ktGrafiklerin çiftleri
app/shared/src/commonMain/kotlin/com/pantrixui/cmp/demo/samples/DiagnosticsSamples.kt, DiagnosticsInspectorSamples.ktDemo'nun iki sayfası
app/shared/src/commonMain/kotlin/com/pantrixui/cmp/demo/App.kt, theme/Theme.kt, shell/SettingsPage.ktDemo'nun kendi tanılaması, inspector'ı ve ayarı

10. Açık kalanlar