From 63708b21a02b65aad3a2977e56d114ac0a1a149d Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 26 Apr 2026 15:14:54 +0700 Subject: [PATCH] fix(folio): testTagsAsResourceId at App root + JS-bridge regression test App.kt sets testTagsAsResourceId=true on the root Box semantics so Compose's testTag surfaces as Android resource-id (and equivalent on iOS via accessibilityIdentifier). Without this, testTag stays in the Compose semantics tree but never reaches the runtime hierarchy that UIAutomator and Sanderling read. Also adds TestStateAxObjectSelectorTestTagAlias as a regression test for the {testTag: ...} object selector resolving through the SDK alias to resource-id at the JS bridge layer. --- .../src/commonMain/kotlin/app/folio/App.kt | 7 ++- internal/verifier/ax_integration_test.go | 52 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/examples/folio/app/shared/src/commonMain/kotlin/app/folio/App.kt b/examples/folio/app/shared/src/commonMain/kotlin/app/folio/App.kt index 8065951..5927ccc 100644 --- a/examples/folio/app/shared/src/commonMain/kotlin/app/folio/App.kt +++ b/examples/folio/app/shared/src/commonMain/kotlin/app/folio/App.kt @@ -16,7 +16,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import app.folio.core.data.DriverFactory import app.folio.core.data.Repository import app.folio.core.data.SqlLedgerStore @@ -32,6 +35,7 @@ import app.folio.ui.theme.LedgerTheme import app.folio.ui.theme.LocalTokens import app.folio.ui.theme.Tokens +@OptIn(ExperimentalComposeUiApi::class) @Composable fun App(driverFactory: DriverFactory) { var component by remember { mutableStateOf(null) } @@ -50,7 +54,8 @@ fun App(driverFactory: DriverFactory) { Modifier .fillMaxSize() .background(t.bg) - .windowInsetsPadding(WindowInsets.safeDrawing), + .windowInsetsPadding(WindowInsets.safeDrawing) + .semantics { testTagsAsResourceId = true }, contentAlignment = Alignment.Center, ) { val c = component diff --git a/internal/verifier/ax_integration_test.go b/internal/verifier/ax_integration_test.go index 1a37e68..fa1b7e9 100644 --- a/internal/verifier/ax_integration_test.go +++ b/internal/verifier/ax_integration_test.go @@ -7,6 +7,58 @@ import ( "github.com/priyanshujain/sanderling/internal/hierarchy" ) +// TestStateAxObjectSelectorTestTagAlias verifies that an object selector +// `{ testTag: "X" }` resolves through the testTag alias to match an element +// whose source attributes carry resource-id="X" (the Compose +// testTagsAsResourceId=true case on Android). +func TestStateAxObjectSelectorTestTagAlias(t *testing.T) { + src := `{ + "attributes": {"class": "android.widget.LinearLayout"}, + "children": [ + { + "attributes": {"resource-id": "LoginScreen", "class": "android.view.View"}, + "children": [ + { + "attributes": {"resource-id": "LoginEmail", "class": "android.widget.EditText"}, + "children": [] + } + ] + } + ] + }` + tree, err := hierarchy.Parse(src) + if err != nil { + t.Fatal(err) + } + + verifier := newVerifier(t) + mustLoad(t, verifier, ` + globalThis.loginRoot = __sanderling__.extract(state => { + const r = state.ax.find({ testTag: "LoginScreen" }); + return r ? "matched" : "miss"; + }); + globalThis.loginEmailViaChain = __sanderling__.extract(state => { + const r = state.ax.find({ testTag: "LoginScreen" }); + if (!r) return "outer-miss"; + const inner = r.find({ testTag: "LoginEmail" }); + return inner ? "inner-matched" : "inner-miss"; + }); + `) + + if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}); err != nil { + t.Fatal(err) + } + + root := verifier.runtime.GlobalObject().Get("loginRoot").ToObject(verifier.runtime).Get("current").String() + if root != "matched" { + t.Fatalf("loginRoot = %q, want matched", root) + } + chain := verifier.runtime.GlobalObject().Get("loginEmailViaChain").ToObject(verifier.runtime).Get("current").String() + if chain != "inner-matched" { + t.Fatalf("loginEmailViaChain = %q, want inner-matched", chain) + } +} + // TestStateAxFindWorks verifies that a Parse+PushSnapshot+extract round trip // actually lets the spec resolve selectors through state.ax.find. // Reads /tmp/live-dump.json (Maestro TreeNode JSON format); skipped if absent.