Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"import": "./dist/src/index.js",
"types": "./dist/src/index.d.ts"
},
"./artifacts": {
"import": "./dist/src/artifacts.js",
"types": "./dist/src/artifacts.d.ts"
},
"./metro": {
"import": "./dist/src/metro.js",
"types": "./dist/src/metro.d.ts"
Expand All @@ -20,9 +24,21 @@
"import": "./dist/src/remote-config.js",
"types": "./dist/src/remote-config.d.ts"
},
"./install-source": {
"import": "./dist/src/install-source.js",
"types": "./dist/src/install-source.d.ts"
},
"./contracts": {
"import": "./dist/src/contracts.js",
"types": "./dist/src/contracts.d.ts"
},
"./selectors": {
"import": "./dist/src/selectors.js",
"types": "./dist/src/selectors.d.ts"
},
"./finders": {
"import": "./dist/src/finders.js",
"types": "./dist/src/finders.d.ts"
}
},
"engines": {
Expand Down
4 changes: 4 additions & 0 deletions rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ export default defineConfig({
source: {
entry: {
index: 'src/index.ts',
artifacts: 'src/artifacts.ts',
metro: 'src/metro.ts',
'remote-config': 'src/remote-config.ts',
'install-source': 'src/install-source.ts',
contracts: 'src/contracts.ts',
selectors: 'src/selectors.ts',
finders: 'src/finders.ts',
},
tsconfigPath: 'tsconfig.lib.json',
},
Expand Down
11 changes: 11 additions & 0 deletions src/__tests__/artifacts-public.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';

import { resolveAndroidArchivePackageName } from '../artifacts.ts';

const resolver: (archivePath: string) => Promise<string | undefined> =
resolveAndroidArchivePackageName;

test('package subpath exports android archive package resolver', () => {
assert.equal(typeof resolver, 'function');
});
36 changes: 36 additions & 0 deletions src/__tests__/finders-public.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import {
findBestMatchesByLocator,
normalizeRole,
normalizeText,
parseFindArgs,
} from '../finders.ts';
import type { SnapshotNode } from '../utils/snapshot.ts';

function makeNode(ref: string, label?: string): SnapshotNode {
return {
index: Number(ref.replace('e', '')) || 0,
ref,
type: 'XCUIElementTypeButton',
label,
};
}

test('public finders entrypoint re-exports pure helpers', () => {
const nodes: SnapshotNode[] = [makeNode('e1', 'Continue')];

const parsed = parseFindArgs(['label', 'Continue', 'click']);
const best = findBestMatchesByLocator(nodes, 'label', 'Continue');
const requireRectLegacy = findBestMatchesByLocator(nodes, 'label', 'Continue', true);
const requireRectOptions = findBestMatchesByLocator(nodes, 'label', 'Continue', {
requireRect: true,
});

assert.equal(normalizeText(' Continue\nNow '), 'continue now');
assert.equal(normalizeRole('XCUIElementTypeApplication.XCUIElementTypeButton'), 'button');
assert.equal(parsed.action, 'click');
assert.equal(best.matches.length, 1);
assert.equal(requireRectLegacy.matches.length, 0);
assert.equal(requireRectOptions.matches.length, 0);
});
19 changes: 19 additions & 0 deletions src/__tests__/install-source-public.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';

import {
ARCHIVE_EXTENSIONS,
isTrustedInstallSourceUrl,
materializeInstallablePath,
validateDownloadSourceUrl,
} from '../install-source.ts';

test('public install-source entrypoint re-exports pure helpers', () => {
assert.deepEqual(ARCHIVE_EXTENSIONS, ['.zip', '.tar', '.tar.gz', '.tgz']);
assert.equal(
isTrustedInstallSourceUrl('https://api.github.com/repos/acme/app/actions/artifacts/1/zip'),
true,
);
assert.equal(typeof materializeInstallablePath, 'function');
assert.equal(typeof validateDownloadSourceUrl, 'function');
});
70 changes: 70 additions & 0 deletions src/__tests__/selectors-public.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import {
findSelectorChainMatch,
formatSelectorFailure,
isNodeEditable,
isNodeVisible,
isSelectorToken,
parseSelectorChain,
resolveSelectorChain,
tryParseSelectorChain,
type Selector,
type SelectorChain,
type SelectorDiagnostics,
type SelectorResolution,
} from '../selectors.ts';
import type { SnapshotNode } from '../utils/snapshot.ts';

const nodes: SnapshotNode[] = [
{
ref: 'e1',
index: 0,
type: 'android.widget.Button',
label: 'Continue',
rect: { x: 0, y: 0, width: 120, height: 48 },
enabled: true,
},
{
ref: 'e2',
index: 1,
type: 'android.widget.EditText',
label: 'Email',
rect: { x: 0, y: 64, width: 200, height: 48 },
enabled: true,
},
];

test('public selector subpath exposes platform-aware matching helpers', () => {
const chain: SelectorChain = parseSelectorChain('role=button label="Continue" visible=true');
const firstSelector: Selector = chain.selectors[0];
assert.equal(firstSelector.raw, 'role=button label="Continue" visible=true');
assert.equal(tryParseSelectorChain(chain.raw)?.raw, chain.raw);
assert.equal(isSelectorToken('visible=true'), true);

const match = findSelectorChainMatch(nodes, chain, {
platform: 'android',
requireRect: true,
});
assert.ok(match);
assert.equal(match.matches, 1);

const resolved: SelectorResolution | null = resolveSelectorChain(nodes, chain, {
platform: 'android',
requireRect: true,
});
assert.equal(resolved?.node.ref, 'e1');

assert.equal(isNodeVisible(nodes[0]), true);
assert.equal(isNodeEditable(nodes[1], 'android'), true);
});

test('public selector diagnostics format failures', () => {
const chain = parseSelectorChain('label=Missing');
const diagnostics: SelectorDiagnostics[] = [{ selector: 'label=Missing', matches: 0 }];

assert.equal(
formatSelectorFailure(chain, diagnostics, { unique: false }),
'Selector did not match (label=Missing -> 0)',
);
});
1 change: 1 addition & 0 deletions src/artifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { resolveAndroidArchivePackageName } from './platforms/android/manifest.ts';
23 changes: 23 additions & 0 deletions src/finders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type { FindLocator } from './utils/finders.ts';
export type { SnapshotNode } from './utils/snapshot.ts';
export { normalizeRole, normalizeText, parseFindArgs } from './utils/finders.ts';

import {
findBestMatchesByLocator as findBestMatchesByLocatorInternal,
type FindLocator,
} from './utils/finders.ts';
import type { SnapshotNode } from './utils/snapshot.ts';

export type FindMatchOptions = {
requireRect?: boolean;
};

export function findBestMatchesByLocator(
nodes: SnapshotNode[],
locator: FindLocator,
query: string,
options?: boolean | FindMatchOptions,
) {
const matchOptions = typeof options === 'boolean' ? { requireRect: options } : options;
return findBestMatchesByLocatorInternal(nodes, locator, query, matchOptions);
}
13 changes: 13 additions & 0 deletions src/install-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export {
ARCHIVE_EXTENSIONS,
isBlockedIpAddress,
isBlockedSourceHostname,
isTrustedInstallSourceUrl,
materializeInstallablePath,
validateDownloadSourceUrl,
} from './platforms/install-source.ts';

export type {
MaterializeInstallSource,
MaterializedInstallable,
} from './platforms/install-source.ts';
18 changes: 18 additions & 0 deletions src/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type {
Selector,
SelectorChain,
SelectorDiagnostics,
SelectorResolution,
} from './daemon/selectors.ts';
export type { SnapshotNode } from './utils/snapshot.ts';

export {
findSelectorChainMatch,
formatSelectorFailure,
isNodeEditable,
isNodeVisible,
isSelectorToken,
parseSelectorChain,
resolveSelectorChain,
tryParseSelectorChain,
} from './daemon/selectors.ts';
45 changes: 45 additions & 0 deletions website/docs/docs/client-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,32 @@ Public subpath API exposed for Node consumers:
- types: `RemoteConfigProfile`, `RemoteConfigProfileOptions`, `ResolvedRemoteConfigProfile`
- `agent-device/contracts`
- types: `SessionRuntimeHints`, `DaemonInstallSource`, `DaemonLockPolicy`, `DaemonRequestMeta`, `DaemonRequest`, `DaemonArtifact`, `DaemonResponseData`, `DaemonError`, `DaemonResponse`
- `agent-device/selectors`
- `parseSelectorChain(expression)`
- `tryParseSelectorChain(expression)`
- `resolveSelectorChain(nodes, chain, options)`
- `findSelectorChainMatch(nodes, chain, options)`
- `formatSelectorFailure(chain, diagnostics, options)`
- `isNodeVisible(node)`
- `isSelectorToken(token)`
- `isNodeEditable(node, platform)`
- types: `Selector`, `SelectorChain`, `SelectorDiagnostics`, `SelectorResolution`, `SnapshotNode`
- `agent-device/finders`
- `findBestMatchesByLocator(nodes, locator, query, requireRectOrOptions)`
- `normalizeRole(value)`
- `normalizeText(value)`
- `parseFindArgs(args)`
- types: `FindLocator`, `FindMatchOptions`, `SnapshotNode`
- `agent-device/install-source`
- `ARCHIVE_EXTENSIONS`
- `isBlockedIpAddress(address)`
- `isBlockedSourceHostname(hostname)`
- `isTrustedInstallSourceUrl(sourceUrl)`
- `materializeInstallablePath(options)`
- `validateDownloadSourceUrl(url)`
- types: `MaterializeInstallSource`, `MaterializedInstallable`
- `agent-device/artifacts`
- `resolveAndroidArchivePackageName(archivePath)`

## Basic usage

Expand Down Expand Up @@ -189,3 +215,22 @@ await stopMetroTunnel({
```

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.

## Selector helpers

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.

```ts
import { findSelectorChainMatch, parseSelectorChain } from 'agent-device/selectors';

const chain = parseSelectorChain('role=button label="Continue" visible=true');

const match = findSelectorChainMatch(snapshot.nodes, chain, {
platform: 'android',
requireRect: true,
});

if (!match) {
// Build a daemon-shaped error with formatSelectorFailure(...) if needed.
}
```
Loading