peachsim 0.1.628 → 0.1.630

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 (111) hide show
  1. package/README.md +8 -4
  2. package/cli/bridge-flow-runner.ts +10 -3
  3. package/cli/cli-update.ts +2 -7
  4. package/cli/cloud-test-tunnel.ts +16 -3
  5. package/cli/commands/config.ts +1 -1
  6. package/cli/commands/control.ts +28 -3
  7. package/cli/commands/daemon-mac-app.ts +4 -2
  8. package/cli/commands/daemon.ts +4 -4
  9. package/cli/commands/device.ts +2 -3
  10. package/cli/commands/inspect/actions.ts +125 -90
  11. package/cli/commands/inspect/core.ts +5 -0
  12. package/cli/commands/inspect.ts +27 -13
  13. package/cli/commands/platform.ts +1 -1
  14. package/cli/commands/screenshot-layers.ts +1 -1
  15. package/cli/commands/screenshot.ts +1 -1
  16. package/cli/commands/skills.ts +0 -31
  17. package/cli/drivers/playwright-provisioning.ts +3 -2
  18. package/cli/drivers/playwright-sim-host.ts +25 -9
  19. package/cli/main.ts +8 -2
  20. package/cli/outbound-endpoints.ts +0 -4
  21. package/detox/index.ts +74 -43
  22. package/dist-lib/agent-daemon-client.cjs +1 -1
  23. package/dist-lib/agent-events.cjs +1 -1
  24. package/dist-lib/agent-identity.cjs +1 -1
  25. package/dist-lib/agent-sessions.cjs +1 -1
  26. package/dist-lib/attached-projects.cjs +1 -1
  27. package/dist-lib/auth/shared-session.cjs +1 -1
  28. package/dist-lib/backend-origin.cjs +1 -1
  29. package/dist-lib/beta.cjs +1 -1
  30. package/dist-lib/beta.mjs +1 -1
  31. package/dist-lib/bridge-constants.cjs +1 -1
  32. package/dist-lib/bridge-contract-input.cjs +1 -1
  33. package/dist-lib/bridge-contract-input.mjs +1 -1
  34. package/dist-lib/bridge-contract.cjs +1 -1
  35. package/dist-lib/bridge-contract.mjs +1 -1
  36. package/dist-lib/capture-contract.cjs +1 -1
  37. package/dist-lib/capture-contract.mjs +1 -1
  38. package/dist-lib/cli-constants.cjs +1 -1
  39. package/dist-lib/cloud-contract.cjs +1 -1
  40. package/dist-lib/cloud-contract.mjs +1 -1
  41. package/dist-lib/cloud-test-args.cjs +1 -1
  42. package/dist-lib/cloud-test-args.mjs +1 -1
  43. package/dist-lib/cloud.cjs +1 -1
  44. package/dist-lib/cloud.mjs +1 -1
  45. package/dist-lib/config.cjs +1 -1
  46. package/dist-lib/detox/index.cjs +129 -27
  47. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  48. package/dist-lib/home-paths.cjs +1 -1
  49. package/dist-lib/host/bridge-host.cjs +290 -315
  50. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  51. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  52. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  53. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  54. package/dist-lib/host/websocket-proxy.cjs +1 -1
  55. package/dist-lib/index.cjs +253 -121
  56. package/dist-lib/jump-to-source-babel.cjs +1 -1
  57. package/dist-lib/jump-to-source-native.cjs +1 -1
  58. package/dist-lib/{vite-kotlin.cjs → kotlin-host.cjs} +130 -19
  59. package/dist-lib/menu.cjs +1 -1
  60. package/dist-lib/menu.mjs +1 -1
  61. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  62. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  63. package/dist-lib/metro-production-bundle.cjs +1 -1
  64. package/dist-lib/metro-production-bundle.mjs +1 -1
  65. package/dist-lib/metro.cjs +132 -9
  66. package/dist-lib/native-app-identity.cjs +278 -0
  67. package/dist-lib/native-app-identity.mjs +245 -0
  68. package/dist-lib/profiles.cjs +1 -1
  69. package/dist-lib/public-brand.cjs +1 -1
  70. package/dist-lib/react-native-host-modules.cjs +1 -1
  71. package/dist-lib/react-native-host-modules.mjs +1 -1
  72. package/dist-lib/render-mode.cjs +1 -1
  73. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  74. package/dist-lib/sdk.cjs +110 -87
  75. package/dist-lib/sdk.mjs +110 -87
  76. package/dist-lib/skills.cjs +841 -643
  77. package/dist-lib/swift-compile.cjs +1 -1
  78. package/dist-lib/swift-compile.mjs +1 -1
  79. package/dist-lib/swift-package.cjs +1 -1
  80. package/dist-lib/swift-package.mjs +1 -1
  81. package/dist-lib/swift-submit.cjs +1 -1
  82. package/dist-lib/swift-submit.mjs +1 -1
  83. package/dist-lib/swift.cjs +282 -177
  84. package/dist-lib/vite-swift.cjs +1 -1
  85. package/dist-lib/vite.cjs +11 -28
  86. package/kotlin/README.md +32 -4
  87. package/kotlin/fixtures/counter/Counter.kt +26 -0
  88. package/kotlin/fixtures/counter/CounterScreen.kt +40 -0
  89. package/kotlin/fixtures/headless-counter/Headless.kt +228 -0
  90. package/kotlin/fixtures/runtime-state/RuntimeState.kt +200 -0
  91. package/kotlin/fork/LICENSE +178 -0
  92. package/kotlin/fork/NOTICE +25 -0
  93. package/kotlin/fork/README.md +59 -0
  94. package/kotlin/fork/build.sh +128 -0
  95. package/kotlin/fork/compose-worker-globals.patch +96 -0
  96. package/kotlin/fork/fork.json +33 -0
  97. package/kotlin/fork/publish.ts +77 -0
  98. package/kotlin/fork/skiko-worker-clock.patch +33 -0
  99. package/kotlin/toolchain.json +288 -0
  100. package/package.json +12 -5
  101. package/src/automatic-update-policy.ts +17 -0
  102. package/src/host/bridge-host.ts +3 -1
  103. package/src/{vite-plugin-kotlin.ts → kotlin-host.ts} +155 -12
  104. package/src/metro-plugin.ts +18 -0
  105. package/src/native-app-identity.ts +376 -0
  106. package/src/native-firebase-config.ts +90 -0
  107. package/src/screenshots/frame-compose.ts +5 -2
  108. package/src/screenshots/registry.ts +1 -1
  109. package/src/screenshots/schema.ts +2 -2
  110. package/src/vite-plugin-one.ts +11 -28
  111. package/src/vite-plugin.ts +6 -1
package/dist-lib/sdk.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.628 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.630 | (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;
@@ -1799,16 +1799,26 @@ function payloadFromResolved(resolved, match, extra = {}) {
1799
1799
  ...extra
1800
1800
  };
1801
1801
  }
1802
- function collectTextCandidates(nodes, ancestors = [], pressableOwnerText = "") {
1802
+ function collectTextCandidates(nodes, ancestors = [], pressableOwnerText = "", actionableAncestor = false, actionableRole) {
1803
1803
  const out = [];
1804
1804
  for (const node of nodes) {
1805
1805
  const text = nodeText(node);
1806
1806
  const caption = !!text && text === pressableOwnerText && !node.pressable;
1807
- if (!caption) out.push({ node, ancestorTestIDs: ancestors });
1807
+ const actionable = !!node.pressable || actionableAncestor;
1808
+ const role = node.role ?? actionableRole;
1809
+ if (!caption) out.push({ node, ancestorTestIDs: ancestors, actionable, role });
1808
1810
  if (node.children?.length) {
1809
1811
  const next = node.testID ? [...ancestors, node.testID] : ancestors;
1810
1812
  const owner = text ? node.pressable ? text : "" : pressableOwnerText;
1811
- out.push(...collectTextCandidates(node.children, next, owner));
1813
+ out.push(
1814
+ ...collectTextCandidates(
1815
+ node.children,
1816
+ next,
1817
+ owner,
1818
+ actionable,
1819
+ node.pressable ? node.role : actionableRole
1820
+ )
1821
+ );
1812
1822
  }
1813
1823
  }
1814
1824
  return out;
@@ -1832,85 +1842,96 @@ async function tapByText(bridge, query, options = {}, opts = {}) {
1832
1842
  agent: opts.agent,
1833
1843
  timing: opts.timing,
1834
1844
  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
- };
1845
+ resolve: () => resolveTextTapTarget(bridge, query, options)
1846
+ });
1847
+ }
1848
+ async function resolveTextTapTarget(bridge, query, options = {}) {
1849
+ const { tree } = await inspectTree(bridge, 50);
1850
+ if (!Array.isArray(tree)) return { matched: 0, total: 0 };
1851
+ const needle = options.exact ? query : query.toLowerCase();
1852
+ let candidates = collectTextCandidates(tree).filter(
1853
+ ({ node: node2, ancestorTestIDs, role }) => {
1854
+ const text = nodeText(node2);
1855
+ if (!text) return false;
1856
+ if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
1857
+ return false;
1894
1858
  }
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
- };
1859
+ if (options.role && role !== options.role) return false;
1860
+ if (options.within && !ancestorTestIDs.includes(options.within)) return false;
1861
+ const ax = node2.geometry.x;
1862
+ const ay = node2.geometry.y;
1863
+ if (options.minX != null && !(ax >= options.minX)) return false;
1864
+ if (options.maxX != null && !(ax <= options.maxX)) return false;
1865
+ if (options.minY != null && !(ay >= options.minY)) return false;
1866
+ if (options.maxY != null && !(ay <= options.maxY)) return false;
1867
+ return true;
1868
+ }
1869
+ );
1870
+ if (options.nth == null && !options.first && !options.near) {
1871
+ const actionable = candidates.filter((candidate) => candidate.actionable);
1872
+ if (actionable.length > 0) candidates = actionable;
1873
+ }
1874
+ if (options.near) {
1875
+ const { x, y } = options.near;
1876
+ candidates.sort(
1877
+ (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)
1878
+ );
1879
+ } else {
1880
+ candidates.sort((a, b) => {
1881
+ if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
1882
+ return a.node.geometry.y - b.node.geometry.y;
1910
1883
  }
1911
- return payloadFromResolved(resolved, node, { total, idx });
1884
+ return a.node.geometry.x - b.node.geometry.x;
1885
+ });
1886
+ }
1887
+ const total = candidates.length;
1888
+ if (total === 0) return { matched: 0, total: 0 };
1889
+ let idx = 0;
1890
+ if (options.nth != null) {
1891
+ idx = options.nth < 0 ? total + options.nth : options.nth;
1892
+ if (idx < 0 || idx >= total) {
1893
+ return { matched: 0, total, nthOutOfRange: true, nth: options.nth };
1912
1894
  }
1895
+ } else if (total > 1 && !options.first && !options.near) {
1896
+ return {
1897
+ ambiguous: true,
1898
+ total,
1899
+ candidates: candidates.slice(0, 10).map((candidate, index) => ({
1900
+ idx: index,
1901
+ nodeId: candidate.node.nodeId,
1902
+ type: candidate.node.type,
1903
+ testID: candidate.node.testID ?? null,
1904
+ text: nodeText(candidate.node).slice(0, 60),
1905
+ abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
1906
+ layout: {
1907
+ width: Math.round(candidate.node.geometry.width),
1908
+ height: Math.round(candidate.node.geometry.height)
1909
+ },
1910
+ ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5)
1911
+ }))
1912
+ };
1913
+ }
1914
+ const picked = candidates[idx];
1915
+ if (!picked) return { matched: 0, total };
1916
+ const node = picked.node;
1917
+ const tap = await bridge.send({
1918
+ type: "call",
1919
+ path: "SootSim.bridges.test.resolveTapTarget",
1920
+ args: [node.nodeId]
1913
1921
  });
1922
+ if (!isTapTargetInfo(tap)) return { matched: 0, total };
1923
+ return payloadFromResolved(
1924
+ {
1925
+ x: tap.cx,
1926
+ y: tap.cy,
1927
+ nodeId: tap.target.nodeId,
1928
+ testID: tap.target.testID,
1929
+ text: tap.target.text ?? tap.target.accessibilityLabel,
1930
+ type: tap.target.type
1931
+ },
1932
+ node,
1933
+ { total, idx }
1934
+ );
1914
1935
  }
1915
1936
  var TAP_QUERY_FINDERS = {
1916
1937
  label: "findByLabel",
@@ -1974,17 +1995,19 @@ async function tapBest(bridge, query, opts = {}) {
1974
1995
  }
1975
1996
  };
1976
1997
  }
1977
- const byText = await resolveTargetCoords(bridge, { mode: "text", value: query });
1978
- if (byText) {
1998
+ const byText = await resolveTextTapTarget(bridge, query);
1999
+ if ("ambiguous" in byText) {
2000
+ return {
2001
+ ambiguous: true,
2002
+ error: `multiple text matches for "${query}"; use tap-text with --nth, --within or --near`
2003
+ };
2004
+ }
2005
+ if ("cx" in byText && typeof byText.cx === "number") {
1979
2006
  return {
1980
- ...payloadFromResolved(byText, byText),
2007
+ ...byText,
1981
2008
  strategy: "text",
1982
2009
  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
2010
+ ...byText.target
1988
2011
  }
1989
2012
  };
1990
2013
  }
package/dist-lib/sdk.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.628 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.630 | (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
  }