Skip to content

Commit 6cf63c2

Browse files
authored
feat: expose selector helper subpath (#384)
1 parent e9c4730 commit 6cf63c2

7 files changed

Lines changed: 124 additions & 2 deletions

File tree

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
"./contracts": {
2424
"import": "./dist/src/contracts.js",
2525
"types": "./dist/src/contracts.d.ts"
26+
},
27+
"./selectors": {
28+
"import": "./dist/src/selectors.js",
29+
"types": "./dist/src/selectors.d.ts"
2630
}
2731
},
2832
"engines": {

rslib.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default defineConfig({
2020
metro: 'src/metro.ts',
2121
'remote-config': 'src/remote-config.ts',
2222
contracts: 'src/contracts.ts',
23+
selectors: 'src/selectors.ts',
2324
},
2425
tsconfigPath: 'tsconfig.lib.json',
2526
},
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { test } from 'vitest';
2+
import assert from 'node:assert/strict';
3+
import {
4+
findSelectorChainMatch,
5+
formatSelectorFailure,
6+
isNodeEditable,
7+
isNodeVisible,
8+
isSelectorToken,
9+
parseSelectorChain,
10+
resolveSelectorChain,
11+
tryParseSelectorChain,
12+
type Selector,
13+
type SelectorChain,
14+
type SelectorDiagnostics,
15+
type SelectorResolution,
16+
} from '../selectors.ts';
17+
import type { SnapshotNode } from '../utils/snapshot.ts';
18+
19+
const nodes: SnapshotNode[] = [
20+
{
21+
ref: 'e1',
22+
index: 0,
23+
type: 'android.widget.Button',
24+
label: 'Continue',
25+
rect: { x: 0, y: 0, width: 120, height: 48 },
26+
enabled: true,
27+
},
28+
{
29+
ref: 'e2',
30+
index: 1,
31+
type: 'android.widget.EditText',
32+
label: 'Email',
33+
rect: { x: 0, y: 64, width: 200, height: 48 },
34+
enabled: true,
35+
},
36+
];
37+
38+
test('public selector subpath exposes platform-aware matching helpers', () => {
39+
const chain: SelectorChain = parseSelectorChain('role=button label="Continue" visible=true');
40+
const firstSelector: Selector = chain.selectors[0];
41+
assert.equal(firstSelector.raw, 'role=button label="Continue" visible=true');
42+
assert.equal(tryParseSelectorChain(chain.raw)?.raw, chain.raw);
43+
assert.equal(isSelectorToken('visible=true'), true);
44+
45+
const match = findSelectorChainMatch(nodes, chain, {
46+
platform: 'android',
47+
requireRect: true,
48+
});
49+
assert.ok(match);
50+
assert.equal(match.matches, 1);
51+
52+
const resolved: SelectorResolution | null = resolveSelectorChain(nodes, chain, {
53+
platform: 'android',
54+
requireRect: true,
55+
});
56+
assert.equal(resolved?.node.ref, 'e1');
57+
58+
assert.equal(isNodeVisible(nodes[0]), true);
59+
assert.equal(isNodeEditable(nodes[1], 'android'), true);
60+
});
61+
62+
test('public selector diagnostics format failures', () => {
63+
const chain = parseSelectorChain('label=Missing');
64+
const diagnostics: SelectorDiagnostics[] = [{ selector: 'label=Missing', matches: 0 }];
65+
66+
assert.equal(
67+
formatSelectorFailure(chain, diagnostics, { unique: false }),
68+
'Selector did not match (label=Missing -> 0)',
69+
);
70+
});

src/daemon/selectors-resolve.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export type SelectorDiagnostics = {
88
matches: number;
99
};
1010

11-
type SelectorResolution = {
11+
export type SelectorResolution = {
1212
node: SnapshotNode;
1313
selector: Selector;
1414
selectorIndex: number;

src/daemon/selectors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
export type { SelectorChain } from './selectors-parse.ts';
1+
export type { Selector, SelectorChain } from './selectors-parse.ts';
2+
export type { SelectorDiagnostics, SelectorResolution } from './selectors-resolve.ts';
23

34
export {
45
parseSelectorChain,

src/selectors.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export type {
2+
Selector,
3+
SelectorChain,
4+
SelectorDiagnostics,
5+
SelectorResolution,
6+
} from './daemon/selectors.ts';
7+
8+
export {
9+
findSelectorChainMatch,
10+
formatSelectorFailure,
11+
isNodeEditable,
12+
isNodeVisible,
13+
isSelectorToken,
14+
parseSelectorChain,
15+
resolveSelectorChain,
16+
tryParseSelectorChain,
17+
} from './daemon/selectors.ts';

website/docs/docs/client-api.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ Public subpath API exposed for Node consumers:
2323
- types: `RemoteConfigProfile`, `RemoteConfigProfileOptions`, `ResolvedRemoteConfigProfile`
2424
- `agent-device/contracts`
2525
- types: `SessionRuntimeHints`, `DaemonInstallSource`, `DaemonLockPolicy`, `DaemonRequestMeta`, `DaemonRequest`, `DaemonArtifact`, `DaemonResponseData`, `DaemonError`, `DaemonResponse`
26+
- `agent-device/selectors`
27+
- `parseSelectorChain(expression)`
28+
- `tryParseSelectorChain(expression)`
29+
- `resolveSelectorChain(nodes, chain, options)`
30+
- `findSelectorChainMatch(nodes, chain, options)`
31+
- `formatSelectorFailure(chain, diagnostics, options)`
32+
- `isNodeVisible(node)`
33+
- `isSelectorToken(token)`
34+
- `isNodeEditable(node, platform)`
35+
- types: `Selector`, `SelectorChain`, `SelectorDiagnostics`, `SelectorResolution`
2636

2737
## Basic usage
2838

@@ -189,3 +199,22 @@ await stopMetroTunnel({
189199
```
190200

191201
Use `agent-device/remote-config` for profile loading and path resolution, `agent-device/metro` for Metro preparation and tunnel lifecycle, and `agent-device/contracts` when a server consumer needs daemon request or runtime contract types.
202+
203+
## Selector helpers
204+
205+
Use `agent-device/selectors` when a remote daemon or bridge needs to parse and match selector expressions without deep-importing daemon internals. Matching is platform-aware because role normalization and editability checks differ by backend.
206+
207+
```ts
208+
import { findSelectorChainMatch, parseSelectorChain } from 'agent-device/selectors';
209+
210+
const chain = parseSelectorChain('role=button label="Continue" visible=true');
211+
212+
const match = findSelectorChainMatch(snapshot.nodes, chain, {
213+
platform: 'android',
214+
requireRect: true,
215+
});
216+
217+
if (!match) {
218+
// Build a daemon-shaped error with formatSelectorFailure(...) if needed.
219+
}
220+
```

0 commit comments

Comments
 (0)