peachsim 0.1.629 → 0.1.631

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +8 -4
  2. package/cli/app-fonts.ts +23 -4
  3. package/cli/bridge-flow-runner.ts +45 -31
  4. package/cli/cli-update.ts +2 -7
  5. package/cli/cloud-test-tunnel.ts +16 -3
  6. package/cli/command-registry.ts +1 -0
  7. package/cli/commands/compat.ts +11 -60
  8. package/cli/commands/config.ts +1 -1
  9. package/cli/commands/control.ts +40 -8
  10. package/cli/commands/daemon-mac-app.ts +4 -2
  11. package/cli/commands/daemon.ts +4 -4
  12. package/cli/commands/detect.ts +289 -26
  13. package/cli/commands/device.ts +2 -3
  14. package/cli/commands/inspect/actions.ts +125 -90
  15. package/cli/commands/inspect/core.ts +1 -1
  16. package/cli/commands/inspect.ts +10 -1
  17. package/cli/commands/maestro.ts +3 -1
  18. package/cli/commands/platform.ts +1 -1
  19. package/cli/commands/project-report.ts +386 -0
  20. package/cli/commands/projectReportContract.ts +42 -0
  21. package/cli/commands/report-issue.ts +38 -54
  22. package/cli/commands/screenshot-layers.ts +1 -1
  23. package/cli/commands/screenshot.ts +1 -1
  24. package/cli/commands/setup.ts +6 -4
  25. package/cli/commands/skills.ts +0 -31
  26. package/cli/commands/telemetry.ts +8 -3
  27. package/cli/commands/test.ts +347 -134
  28. package/cli/drivers/playwright-provisioning.ts +3 -2
  29. package/cli/drivers/playwright-sim-host.ts +25 -9
  30. package/cli/main.ts +19 -3
  31. package/cli/outbound-endpoints.ts +2 -5
  32. package/cli/privacy.ts +1 -2
  33. package/cli/runtime-diagnostics.ts +51 -0
  34. package/cli/telemetry.ts +78 -7
  35. package/detox/index.ts +77 -51
  36. package/dist-lib/agent-daemon-client.cjs +1 -1
  37. package/dist-lib/agent-events.cjs +1 -1
  38. package/dist-lib/agent-identity.cjs +1 -1
  39. package/dist-lib/agent-sessions.cjs +1 -1
  40. package/dist-lib/attached-projects.cjs +1 -1
  41. package/dist-lib/auth/shared-session.cjs +1 -1
  42. package/dist-lib/backend-origin.cjs +1 -1
  43. package/dist-lib/beta.cjs +1 -1
  44. package/dist-lib/beta.mjs +1 -1
  45. package/dist-lib/bridge-constants.cjs +1 -1
  46. package/dist-lib/bridge-contract-input.cjs +1 -1
  47. package/dist-lib/bridge-contract-input.mjs +1 -1
  48. package/dist-lib/bridge-contract.cjs +1 -1
  49. package/dist-lib/bridge-contract.mjs +1 -1
  50. package/dist-lib/capture-contract.cjs +1 -1
  51. package/dist-lib/capture-contract.mjs +1 -1
  52. package/dist-lib/cli-constants.cjs +1 -1
  53. package/dist-lib/cloud-contract.cjs +1 -1
  54. package/dist-lib/cloud-contract.mjs +1 -1
  55. package/dist-lib/cloud-test-args.cjs +1 -1
  56. package/dist-lib/cloud-test-args.mjs +1 -1
  57. package/dist-lib/cloud.cjs +1 -1
  58. package/dist-lib/cloud.mjs +1 -1
  59. package/dist-lib/config.cjs +1 -1
  60. package/dist-lib/detox/index.cjs +130 -33
  61. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  62. package/dist-lib/home-paths.cjs +8 -5
  63. package/dist-lib/host/bridge-host.cjs +353 -290
  64. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  65. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  66. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  67. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  68. package/dist-lib/host/websocket-proxy.cjs +1 -1
  69. package/dist-lib/index.cjs +257 -121
  70. package/dist-lib/jump-to-source-babel.cjs +1 -1
  71. package/dist-lib/jump-to-source-native.cjs +1 -1
  72. package/dist-lib/{vite-kotlin.cjs → kotlin-host.cjs} +130 -19
  73. package/dist-lib/menu.cjs +11 -1
  74. package/dist-lib/menu.mjs +11 -1
  75. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  76. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  77. package/dist-lib/metro-production-bundle.cjs +1 -1
  78. package/dist-lib/metro-production-bundle.mjs +1 -1
  79. package/dist-lib/metro.cjs +132 -9
  80. package/dist-lib/native-app-identity.cjs +278 -0
  81. package/dist-lib/native-app-identity.mjs +245 -0
  82. package/dist-lib/profiles.cjs +1 -1
  83. package/dist-lib/public-brand.cjs +1 -1
  84. package/dist-lib/react-native-host-modules.cjs +1 -1
  85. package/dist-lib/react-native-host-modules.mjs +1 -1
  86. package/dist-lib/render-mode.cjs +1 -1
  87. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  88. package/dist-lib/sdk.cjs +112 -87
  89. package/dist-lib/sdk.mjs +111 -87
  90. package/dist-lib/skills.cjs +1270 -709
  91. package/dist-lib/swift-compile.cjs +1 -1
  92. package/dist-lib/swift-compile.mjs +1 -1
  93. package/dist-lib/swift-package.cjs +1 -1
  94. package/dist-lib/swift-package.mjs +1 -1
  95. package/dist-lib/swift-submit.cjs +1 -1
  96. package/dist-lib/swift-submit.mjs +1 -1
  97. package/dist-lib/swift.cjs +672 -275
  98. package/dist-lib/vite-swift.cjs +1 -1
  99. package/dist-lib/vite.cjs +11 -28
  100. package/kotlin/README.md +32 -4
  101. package/kotlin/fixtures/counter/Counter.kt +26 -0
  102. package/kotlin/fixtures/counter/CounterScreen.kt +40 -0
  103. package/kotlin/fixtures/headless-counter/Headless.kt +228 -0
  104. package/kotlin/fixtures/runtime-state/RuntimeState.kt +200 -0
  105. package/kotlin/fork/LICENSE +178 -0
  106. package/kotlin/fork/NOTICE +25 -0
  107. package/kotlin/fork/README.md +59 -0
  108. package/kotlin/fork/build.sh +128 -0
  109. package/kotlin/fork/compose-worker-globals.patch +96 -0
  110. package/kotlin/fork/fork.json +33 -0
  111. package/kotlin/fork/publish.ts +77 -0
  112. package/kotlin/fork/skiko-worker-clock.patch +33 -0
  113. package/kotlin/toolchain.json +288 -0
  114. package/package.json +13 -5
  115. package/skills/contrast/SKILL.md +1 -0
  116. package/src/automatic-update-policy.ts +17 -0
  117. package/src/home-paths.ts +11 -9
  118. package/src/host/bridge-host.ts +3 -1
  119. package/src/{vite-plugin-kotlin.ts → kotlin-host.ts} +155 -12
  120. package/src/menu.ts +11 -0
  121. package/src/metro-plugin.ts +18 -0
  122. package/src/native-app-identity.ts +376 -0
  123. package/src/native-firebase-config.ts +90 -0
  124. package/src/screenshots/frame-compose.ts +1 -1
  125. package/src/screenshots/registry.ts +1 -1
  126. package/src/screenshots/schema.ts +2 -2
  127. package/src/vite-plugin-one.ts +11 -28
  128. package/src/vite-plugin.ts +6 -1
package/dist-lib/sdk.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.629 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.631 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -39,6 +39,7 @@ __export(sdk_exports, {
39
39
  clearConsole: () => clearConsole,
40
40
  clearLogs: () => clearLogs,
41
41
  clearRequests: () => clearRequests,
42
+ consoleEntriesEval: () => consoleEntriesEval,
42
43
  detectOpenNativeUI: () => detectOpenNativeUI,
43
44
  filterLogEntries: () => filterLogEntries,
44
45
  formatLayoutElements: () => formatLayoutElements,
@@ -1799,16 +1800,26 @@ function payloadFromResolved(resolved, match, extra = {}) {
1799
1800
  ...extra
1800
1801
  };
1801
1802
  }
1802
- function collectTextCandidates(nodes, ancestors = [], pressableOwnerText = "") {
1803
+ function collectTextCandidates(nodes, ancestors = [], pressableOwnerText = "", actionableAncestor = false, actionableRole) {
1803
1804
  const out = [];
1804
1805
  for (const node of nodes) {
1805
1806
  const text = nodeText(node);
1806
1807
  const caption = !!text && text === pressableOwnerText && !node.pressable;
1807
- if (!caption) out.push({ node, ancestorTestIDs: ancestors });
1808
+ const actionable = !!node.pressable || actionableAncestor;
1809
+ const role = node.role ?? actionableRole;
1810
+ if (!caption) out.push({ node, ancestorTestIDs: ancestors, actionable, role });
1808
1811
  if (node.children?.length) {
1809
1812
  const next = node.testID ? [...ancestors, node.testID] : ancestors;
1810
1813
  const owner = text ? node.pressable ? text : "" : pressableOwnerText;
1811
- out.push(...collectTextCandidates(node.children, next, owner));
1814
+ out.push(
1815
+ ...collectTextCandidates(
1816
+ node.children,
1817
+ next,
1818
+ owner,
1819
+ actionable,
1820
+ node.pressable ? node.role : actionableRole
1821
+ )
1822
+ );
1812
1823
  }
1813
1824
  }
1814
1825
  return out;
@@ -1832,85 +1843,96 @@ async function tapByText(bridge, query, options = {}, opts = {}) {
1832
1843
  agent: opts.agent,
1833
1844
  timing: opts.timing,
1834
1845
  textFallback: query,
1835
- resolve: async () => {
1836
- const { tree } = await inspectTree(bridge, 50);
1837
- if (!Array.isArray(tree)) return { matched: 0, total: 0 };
1838
- const needle = options.exact ? query : query.toLowerCase();
1839
- let candidates = collectTextCandidates(tree).filter(({ node: node2, ancestorTestIDs }) => {
1840
- const text = nodeText(node2);
1841
- if (!text) return false;
1842
- if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
1843
- return false;
1844
- }
1845
- if (options.role && node2.role !== options.role) return false;
1846
- if (options.within && !ancestorTestIDs.includes(options.within)) return false;
1847
- const ax = node2.geometry.x;
1848
- const ay = node2.geometry.y;
1849
- if (options.minX != null && !(ax >= options.minX)) return false;
1850
- if (options.maxX != null && !(ax <= options.maxX)) return false;
1851
- if (options.minY != null && !(ay >= options.minY)) return false;
1852
- if (options.maxY != null && !(ay <= options.maxY)) return false;
1853
- return true;
1854
- });
1855
- if (options.near) {
1856
- const { x, y } = options.near;
1857
- candidates.sort(
1858
- (a, b) => Math.hypot(a.node.geometry.x - x, a.node.geometry.y - y) - Math.hypot(b.node.geometry.x - x, b.node.geometry.y - y)
1859
- );
1860
- } else {
1861
- candidates.sort((a, b) => {
1862
- if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
1863
- return a.node.geometry.y - b.node.geometry.y;
1864
- }
1865
- return a.node.geometry.x - b.node.geometry.x;
1866
- });
1867
- }
1868
- const total = candidates.length;
1869
- if (total === 0) return { matched: 0, total: 0 };
1870
- let idx = 0;
1871
- if (options.nth != null) {
1872
- idx = options.nth < 0 ? total + options.nth : options.nth;
1873
- if (idx < 0 || idx >= total) {
1874
- return { matched: 0, total, nthOutOfRange: true, nth: options.nth };
1875
- }
1876
- } else if (total > 1 && !options.first && !options.near) {
1877
- return {
1878
- ambiguous: true,
1879
- total,
1880
- candidates: candidates.slice(0, 10).map((candidate, index) => ({
1881
- idx: index,
1882
- nodeId: candidate.node.nodeId,
1883
- type: candidate.node.type,
1884
- testID: candidate.node.testID ?? null,
1885
- text: nodeText(candidate.node).slice(0, 60),
1886
- abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
1887
- layout: {
1888
- width: Math.round(candidate.node.geometry.width),
1889
- height: Math.round(candidate.node.geometry.height)
1890
- },
1891
- ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5)
1892
- }))
1893
- };
1846
+ resolve: () => resolveTextTapTarget(bridge, query, options)
1847
+ });
1848
+ }
1849
+ async function resolveTextTapTarget(bridge, query, options = {}) {
1850
+ const { tree } = await inspectTree(bridge, 50);
1851
+ if (!Array.isArray(tree)) return { matched: 0, total: 0 };
1852
+ const needle = options.exact ? query : query.toLowerCase();
1853
+ let candidates = collectTextCandidates(tree).filter(
1854
+ ({ node: node2, ancestorTestIDs, role }) => {
1855
+ const text = nodeText(node2);
1856
+ if (!text) return false;
1857
+ if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
1858
+ return false;
1894
1859
  }
1895
- const picked = candidates[idx];
1896
- if (!picked) return { matched: 0, total };
1897
- const node = picked.node;
1898
- let resolved = node.testID ? await resolveTargetCoords(bridge, { mode: "testid", value: node.testID }) : await resolveTargetCoords(bridge, {
1899
- mode: "text",
1900
- value: nodeText(node) || query
1901
- });
1902
- if (!resolved) {
1903
- resolved = {
1904
- x: node.geometry.x + node.geometry.width / 2,
1905
- y: node.geometry.y + node.geometry.height / 2,
1906
- testID: node.testID ?? null,
1907
- text: nodeText(node) || query,
1908
- type: node.type
1909
- };
1860
+ if (options.role && role !== options.role) return false;
1861
+ if (options.within && !ancestorTestIDs.includes(options.within)) return false;
1862
+ const ax = node2.geometry.x;
1863
+ const ay = node2.geometry.y;
1864
+ if (options.minX != null && !(ax >= options.minX)) return false;
1865
+ if (options.maxX != null && !(ax <= options.maxX)) return false;
1866
+ if (options.minY != null && !(ay >= options.minY)) return false;
1867
+ if (options.maxY != null && !(ay <= options.maxY)) return false;
1868
+ return true;
1869
+ }
1870
+ );
1871
+ if (options.nth == null && !options.first && !options.near) {
1872
+ const actionable = candidates.filter((candidate) => candidate.actionable);
1873
+ if (actionable.length > 0) candidates = actionable;
1874
+ }
1875
+ if (options.near) {
1876
+ const { x, y } = options.near;
1877
+ candidates.sort(
1878
+ (a, b) => Math.hypot(a.node.geometry.x - x, a.node.geometry.y - y) - Math.hypot(b.node.geometry.x - x, b.node.geometry.y - y)
1879
+ );
1880
+ } else {
1881
+ candidates.sort((a, b) => {
1882
+ if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
1883
+ return a.node.geometry.y - b.node.geometry.y;
1910
1884
  }
1911
- return payloadFromResolved(resolved, node, { total, idx });
1885
+ return a.node.geometry.x - b.node.geometry.x;
1886
+ });
1887
+ }
1888
+ const total = candidates.length;
1889
+ if (total === 0) return { matched: 0, total: 0 };
1890
+ let idx = 0;
1891
+ if (options.nth != null) {
1892
+ idx = options.nth < 0 ? total + options.nth : options.nth;
1893
+ if (idx < 0 || idx >= total) {
1894
+ return { matched: 0, total, nthOutOfRange: true, nth: options.nth };
1912
1895
  }
1896
+ } else if (total > 1 && !options.first && !options.near) {
1897
+ return {
1898
+ ambiguous: true,
1899
+ total,
1900
+ candidates: candidates.slice(0, 10).map((candidate, index) => ({
1901
+ idx: index,
1902
+ nodeId: candidate.node.nodeId,
1903
+ type: candidate.node.type,
1904
+ testID: candidate.node.testID ?? null,
1905
+ text: nodeText(candidate.node).slice(0, 60),
1906
+ abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
1907
+ layout: {
1908
+ width: Math.round(candidate.node.geometry.width),
1909
+ height: Math.round(candidate.node.geometry.height)
1910
+ },
1911
+ ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5)
1912
+ }))
1913
+ };
1914
+ }
1915
+ const picked = candidates[idx];
1916
+ if (!picked) return { matched: 0, total };
1917
+ const node = picked.node;
1918
+ const tap = await bridge.send({
1919
+ type: "call",
1920
+ path: "SootSim.bridges.test.resolveTapTarget",
1921
+ args: [node.nodeId]
1913
1922
  });
1923
+ if (!isTapTargetInfo(tap)) return { matched: 0, total };
1924
+ return payloadFromResolved(
1925
+ {
1926
+ x: tap.cx,
1927
+ y: tap.cy,
1928
+ nodeId: tap.target.nodeId,
1929
+ testID: tap.target.testID,
1930
+ text: tap.target.text ?? tap.target.accessibilityLabel,
1931
+ type: tap.target.type
1932
+ },
1933
+ node,
1934
+ { total, idx }
1935
+ );
1914
1936
  }
1915
1937
  var TAP_QUERY_FINDERS = {
1916
1938
  label: "findByLabel",
@@ -1974,17 +1996,19 @@ async function tapBest(bridge, query, opts = {}) {
1974
1996
  }
1975
1997
  };
1976
1998
  }
1977
- const byText = await resolveTargetCoords(bridge, { mode: "text", value: query });
1978
- if (byText) {
1999
+ const byText = await resolveTextTapTarget(bridge, query);
2000
+ if ("ambiguous" in byText) {
2001
+ return {
2002
+ ambiguous: true,
2003
+ error: `multiple text matches for "${query}"; use tap-text with --nth, --within or --near`
2004
+ };
2005
+ }
2006
+ if ("cx" in byText && typeof byText.cx === "number") {
1979
2007
  return {
1980
- ...payloadFromResolved(byText, byText),
2008
+ ...byText,
1981
2009
  strategy: "text",
1982
2010
  node: {
1983
- nodeId: byText.nodeId ?? null,
1984
- id: byText.testID ?? null,
1985
- testID: byText.testID ?? null,
1986
- type: byText.type ?? null,
1987
- text: byText.text ?? query
2011
+ ...byText.target
1988
2012
  }
1989
2013
  };
1990
2014
  }
@@ -2491,6 +2515,7 @@ var SimClient = class {
2491
2515
  clearConsole,
2492
2516
  clearLogs,
2493
2517
  clearRequests,
2518
+ consoleEntriesEval,
2494
2519
  detectOpenNativeUI,
2495
2520
  filterLogEntries,
2496
2521
  formatLayoutElements,
package/dist-lib/sdk.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.629 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.631 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -1701,16 +1701,26 @@ function payloadFromResolved(resolved, match, extra = {}) {
1701
1701
  ...extra
1702
1702
  };
1703
1703
  }
1704
- function collectTextCandidates(nodes, ancestors = [], pressableOwnerText = "") {
1704
+ function collectTextCandidates(nodes, ancestors = [], pressableOwnerText = "", actionableAncestor = false, actionableRole) {
1705
1705
  const out = [];
1706
1706
  for (const node of nodes) {
1707
1707
  const text = nodeText(node);
1708
1708
  const caption = !!text && text === pressableOwnerText && !node.pressable;
1709
- if (!caption) out.push({ node, ancestorTestIDs: ancestors });
1709
+ const actionable = !!node.pressable || actionableAncestor;
1710
+ const role = node.role ?? actionableRole;
1711
+ if (!caption) out.push({ node, ancestorTestIDs: ancestors, actionable, role });
1710
1712
  if (node.children?.length) {
1711
1713
  const next = node.testID ? [...ancestors, node.testID] : ancestors;
1712
1714
  const owner = text ? node.pressable ? text : "" : pressableOwnerText;
1713
- out.push(...collectTextCandidates(node.children, next, owner));
1715
+ out.push(
1716
+ ...collectTextCandidates(
1717
+ node.children,
1718
+ next,
1719
+ owner,
1720
+ actionable,
1721
+ node.pressable ? node.role : actionableRole
1722
+ )
1723
+ );
1714
1724
  }
1715
1725
  }
1716
1726
  return out;
@@ -1734,85 +1744,96 @@ async function tapByText(bridge, query, options = {}, opts = {}) {
1734
1744
  agent: opts.agent,
1735
1745
  timing: opts.timing,
1736
1746
  textFallback: query,
1737
- resolve: async () => {
1738
- const { tree } = await inspectTree(bridge, 50);
1739
- if (!Array.isArray(tree)) return { matched: 0, total: 0 };
1740
- const needle = options.exact ? query : query.toLowerCase();
1741
- let candidates = collectTextCandidates(tree).filter(({ node: node2, ancestorTestIDs }) => {
1742
- const text = nodeText(node2);
1743
- if (!text) return false;
1744
- if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
1745
- return false;
1746
- }
1747
- if (options.role && node2.role !== options.role) return false;
1748
- if (options.within && !ancestorTestIDs.includes(options.within)) return false;
1749
- const ax = node2.geometry.x;
1750
- const ay = node2.geometry.y;
1751
- if (options.minX != null && !(ax >= options.minX)) return false;
1752
- if (options.maxX != null && !(ax <= options.maxX)) return false;
1753
- if (options.minY != null && !(ay >= options.minY)) return false;
1754
- if (options.maxY != null && !(ay <= options.maxY)) return false;
1755
- return true;
1756
- });
1757
- if (options.near) {
1758
- const { x, y } = options.near;
1759
- candidates.sort(
1760
- (a, b) => Math.hypot(a.node.geometry.x - x, a.node.geometry.y - y) - Math.hypot(b.node.geometry.x - x, b.node.geometry.y - y)
1761
- );
1762
- } else {
1763
- candidates.sort((a, b) => {
1764
- if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
1765
- return a.node.geometry.y - b.node.geometry.y;
1766
- }
1767
- return a.node.geometry.x - b.node.geometry.x;
1768
- });
1769
- }
1770
- const total = candidates.length;
1771
- if (total === 0) return { matched: 0, total: 0 };
1772
- let idx = 0;
1773
- if (options.nth != null) {
1774
- idx = options.nth < 0 ? total + options.nth : options.nth;
1775
- if (idx < 0 || idx >= total) {
1776
- return { matched: 0, total, nthOutOfRange: true, nth: options.nth };
1777
- }
1778
- } else if (total > 1 && !options.first && !options.near) {
1779
- return {
1780
- ambiguous: true,
1781
- total,
1782
- candidates: candidates.slice(0, 10).map((candidate, index) => ({
1783
- idx: index,
1784
- nodeId: candidate.node.nodeId,
1785
- type: candidate.node.type,
1786
- testID: candidate.node.testID ?? null,
1787
- text: nodeText(candidate.node).slice(0, 60),
1788
- abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
1789
- layout: {
1790
- width: Math.round(candidate.node.geometry.width),
1791
- height: Math.round(candidate.node.geometry.height)
1792
- },
1793
- ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5)
1794
- }))
1795
- };
1747
+ resolve: () => resolveTextTapTarget(bridge, query, options)
1748
+ });
1749
+ }
1750
+ async function resolveTextTapTarget(bridge, query, options = {}) {
1751
+ const { tree } = await inspectTree(bridge, 50);
1752
+ if (!Array.isArray(tree)) return { matched: 0, total: 0 };
1753
+ const needle = options.exact ? query : query.toLowerCase();
1754
+ let candidates = collectTextCandidates(tree).filter(
1755
+ ({ node: node2, ancestorTestIDs, role }) => {
1756
+ const text = nodeText(node2);
1757
+ if (!text) return false;
1758
+ if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
1759
+ return false;
1796
1760
  }
1797
- const picked = candidates[idx];
1798
- if (!picked) return { matched: 0, total };
1799
- const node = picked.node;
1800
- let resolved = node.testID ? await resolveTargetCoords(bridge, { mode: "testid", value: node.testID }) : await resolveTargetCoords(bridge, {
1801
- mode: "text",
1802
- value: nodeText(node) || query
1803
- });
1804
- if (!resolved) {
1805
- resolved = {
1806
- x: node.geometry.x + node.geometry.width / 2,
1807
- y: node.geometry.y + node.geometry.height / 2,
1808
- testID: node.testID ?? null,
1809
- text: nodeText(node) || query,
1810
- type: node.type
1811
- };
1761
+ if (options.role && role !== options.role) return false;
1762
+ if (options.within && !ancestorTestIDs.includes(options.within)) return false;
1763
+ const ax = node2.geometry.x;
1764
+ const ay = node2.geometry.y;
1765
+ if (options.minX != null && !(ax >= options.minX)) return false;
1766
+ if (options.maxX != null && !(ax <= options.maxX)) return false;
1767
+ if (options.minY != null && !(ay >= options.minY)) return false;
1768
+ if (options.maxY != null && !(ay <= options.maxY)) return false;
1769
+ return true;
1770
+ }
1771
+ );
1772
+ if (options.nth == null && !options.first && !options.near) {
1773
+ const actionable = candidates.filter((candidate) => candidate.actionable);
1774
+ if (actionable.length > 0) candidates = actionable;
1775
+ }
1776
+ if (options.near) {
1777
+ const { x, y } = options.near;
1778
+ candidates.sort(
1779
+ (a, b) => Math.hypot(a.node.geometry.x - x, a.node.geometry.y - y) - Math.hypot(b.node.geometry.x - x, b.node.geometry.y - y)
1780
+ );
1781
+ } else {
1782
+ candidates.sort((a, b) => {
1783
+ if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
1784
+ return a.node.geometry.y - b.node.geometry.y;
1812
1785
  }
1813
- return payloadFromResolved(resolved, node, { total, idx });
1786
+ return a.node.geometry.x - b.node.geometry.x;
1787
+ });
1788
+ }
1789
+ const total = candidates.length;
1790
+ if (total === 0) return { matched: 0, total: 0 };
1791
+ let idx = 0;
1792
+ if (options.nth != null) {
1793
+ idx = options.nth < 0 ? total + options.nth : options.nth;
1794
+ if (idx < 0 || idx >= total) {
1795
+ return { matched: 0, total, nthOutOfRange: true, nth: options.nth };
1814
1796
  }
1797
+ } else if (total > 1 && !options.first && !options.near) {
1798
+ return {
1799
+ ambiguous: true,
1800
+ total,
1801
+ candidates: candidates.slice(0, 10).map((candidate, index) => ({
1802
+ idx: index,
1803
+ nodeId: candidate.node.nodeId,
1804
+ type: candidate.node.type,
1805
+ testID: candidate.node.testID ?? null,
1806
+ text: nodeText(candidate.node).slice(0, 60),
1807
+ abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
1808
+ layout: {
1809
+ width: Math.round(candidate.node.geometry.width),
1810
+ height: Math.round(candidate.node.geometry.height)
1811
+ },
1812
+ ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5)
1813
+ }))
1814
+ };
1815
+ }
1816
+ const picked = candidates[idx];
1817
+ if (!picked) return { matched: 0, total };
1818
+ const node = picked.node;
1819
+ const tap = await bridge.send({
1820
+ type: "call",
1821
+ path: "SootSim.bridges.test.resolveTapTarget",
1822
+ args: [node.nodeId]
1815
1823
  });
1824
+ if (!isTapTargetInfo(tap)) return { matched: 0, total };
1825
+ return payloadFromResolved(
1826
+ {
1827
+ x: tap.cx,
1828
+ y: tap.cy,
1829
+ nodeId: tap.target.nodeId,
1830
+ testID: tap.target.testID,
1831
+ text: tap.target.text ?? tap.target.accessibilityLabel,
1832
+ type: tap.target.type
1833
+ },
1834
+ node,
1835
+ { total, idx }
1836
+ );
1816
1837
  }
1817
1838
  var TAP_QUERY_FINDERS = {
1818
1839
  label: "findByLabel",
@@ -1876,17 +1897,19 @@ async function tapBest(bridge, query, opts = {}) {
1876
1897
  }
1877
1898
  };
1878
1899
  }
1879
- const byText = await resolveTargetCoords(bridge, { mode: "text", value: query });
1880
- if (byText) {
1900
+ const byText = await resolveTextTapTarget(bridge, query);
1901
+ if ("ambiguous" in byText) {
1902
+ return {
1903
+ ambiguous: true,
1904
+ error: `multiple text matches for "${query}"; use tap-text with --nth, --within or --near`
1905
+ };
1906
+ }
1907
+ if ("cx" in byText && typeof byText.cx === "number") {
1881
1908
  return {
1882
- ...payloadFromResolved(byText, byText),
1909
+ ...byText,
1883
1910
  strategy: "text",
1884
1911
  node: {
1885
- nodeId: byText.nodeId ?? null,
1886
- id: byText.testID ?? null,
1887
- testID: byText.testID ?? null,
1888
- type: byText.type ?? null,
1889
- text: byText.text ?? query
1912
+ ...byText.target
1890
1913
  }
1891
1914
  };
1892
1915
  }
@@ -2392,6 +2415,7 @@ export {
2392
2415
  clearConsole,
2393
2416
  clearLogs,
2394
2417
  clearRequests,
2418
+ consoleEntriesEval,
2395
2419
  detectOpenNativeUI,
2396
2420
  filterLogEntries,
2397
2421
  formatLayoutElements,