Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- The mobile a11y snapshot no longer computes the ancestor-role `path` on Android, emitting an empty `path` for every node instead. Android `path` values were widget class names (e.g. `android.widget.FrameLayout`) that dominated the `describe-screen` payload with no targeting value (mobile targeting is by a11y `ref` or `testId`). iOS and browser behavior are unchanged. ([#44](https://github.com/MetaMask/client-mcp-core/pull/44))

## [0.8.0]

### Added
Expand Down
36 changes: 36 additions & 0 deletions src/platform/mobile-platform-driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,42 @@ describe('MobilePlatformDriver', () => {
expect(nodes[2].path).toStrictEqual(['Window']);
});

it('emits an empty path for every node on android', async () => {
const backend = createMockBackend({
platform: 'android',
snapshot: vi.fn().mockResolvedValue({
platform: 'android',
hierarchy: [
makeElement({
type: 'android.widget.FrameLayout',
children: [
makeElement({
type: 'android.widget.LinearLayout',
children: [
makeElement({ type: 'android.widget.Button', label: 'OK' }),
makeElement({
type: 'android.widget.Button',
label: 'Cancel',
}),
],
}),
],
}),
],
raw: '[]',
timestamp: Date.now(),
}),
});
const driver = new MobilePlatformDriver(backend);

const { nodes } = await driver.getAccessibilityTree();

expect(nodes).toHaveLength(4);
for (const node of nodes) {
expect(node.path).toStrictEqual([]);
}
});

it('builds refMap with value fallback', async () => {
const backend = createMockBackend({
snapshot: vi.fn().mockResolvedValue({
Expand Down
20 changes: 16 additions & 4 deletions src/platform/mobile-platform-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export class MobilePlatformDriver implements IPlatformDriver {
_rootSelector?: string,
): Promise<{ nodes: A11yNodeTrimmed[]; refMap: Map<string, string> }> {
const snapshot = await this.#backend.snapshot();
return normalizeSnapshot(snapshot.hierarchy);
return normalizeSnapshot(snapshot.hierarchy, this.#backend.platform);
}

/**
Expand Down Expand Up @@ -685,21 +685,33 @@ function parseStableIdentifier(stableId: string): ElementQuery {
/**
* Normalizes a UIElement hierarchy into A11yNodeTrimmed nodes with sequential refs.
*
* On Android the ancestor-role `path` is a chain of widget class names
* (e.g. `android.widget.FrameLayout`) that offers no targeting value — mobile
* targeting is by a11y `ref` or `testId` only — and dominates the payload
* returned to agents. So on Android we emit an empty `path` rather than
* accumulating the ancestry. iOS keeps the ancestor path (its parity is
* unvalidated), matching the browser driver's behavior.
*
* @param hierarchy - The raw UIElement tree from a device snapshot.
* @param platform - The device platform; `'android'` skips path computation.
* @returns Trimmed nodes and the ref-to-stable-identifier map.
*/
function normalizeSnapshot(hierarchy: UIElement[]): {
function normalizeSnapshot(
hierarchy: UIElement[],
platform: PlatformType,
): {
nodes: A11yNodeTrimmed[];
refMap: Map<string, string>;
} {
const nodes: A11yNodeTrimmed[] = [];
const refMap = new Map<string, string>();
const stableIdCount = new Map<string, string[]>();
const skipPath = platform === 'android';
let refCounter = 0;

/**
* @param elements - UIElement nodes to walk.
* @param path - Accumulated ancestor roles.
* @param path - Accumulated ancestor roles (always empty on Android).
*/
function walk(elements: UIElement[], path: string[]): void {
for (const el of elements) {
Expand Down Expand Up @@ -737,7 +749,7 @@ function normalizeSnapshot(hierarchy: UIElement[]): {
}

if (el.children?.length) {
walk(el.children, [...path, role]);
walk(el.children, skipPath ? path : [...path, role]);
}
}
}
Expand Down
Loading