Files
software-mansion__argent/packages/tool-server/test/describe-native-adapter.test.ts
Ignacy Łątka d017ba340c feat: Android emulator support (#148)
### For reviewers - what to focus on:

The diff is ~6k LoC; this breakdown should help triage where to spend
attention:

| Bucket | Lines | Effort |
| --- | --- | --- |
| **Net-new Android logic**: `boot-device.ts` (660) + `adb.ts` (399) +
`uiautomator-parser.ts` (292) | 1351 | **Read carefully** |
| Blueprint `DeviceInfo` refactor: `simulator-server`, `ax-service`,
`native-devtools` | 365 | Read |
| Tool `index.ts` / `types.ts` sprawl across ~20 tools (capability
gates, `services()`, descriptions) | ~965 | Skim — watch for cross-file
consistency |
| `platforms/android.ts` impls (stub → real implementation) | ~426 |
Skim |
| Skill/docs prose, profiler rename ripple | ~1000 | Skim |
| **`platforms/ios.ts` deletions** — 12 files, 0 additions; logic moved
into unified handlers | 684 | **Skip — mechanical** |

## What works, what doesn't:

### Just works thanks to either sim server or metro:
- `gesture-*`
- `button`, `keyboard`
- `screenshot`
- `run-sequence`, `flow-*`
- `debugger-*`
- `react-profiler-*`

### Adjusted to work on android too:

| tool | android | ios |
| --- | --- | --- |
| `describe` | `uiautomator dump` → `DescribeNode` | `AXServiceApi` |
| `launch-app` | `cmd package resolve-activity` + `am start -W` |
`simctl launch` |
| `restart-app` | `cmd package resolve-activity` + `am force-stop` + `am
start -W` | `simctl terminate` + `simctl launch` |
| `reinstall-app` | `adb install -r` (+`-g`/`-d`) | `simctl uninstall` +
`simctl install` |
| `open-url` | `am start -a VIEW -d <url>` | `simctl openurl` |
| `list-devices` | `adb devices` + `emulator -list-avds` | `xcrun simctl
list --json` |
| `boot-device` | snapshot probe → `-force-snapshot-load` hot boot with
screencap-alive guard, falls back to cold boot on any failure | `simctl
boot` + `bootstatus -b` |

### Will not work on android for now:
- `native-devtools-*` (7 total)
- `native-profiler-*` (3 total — was `ios-profiler-*`)
- `profiler-stack-query`, `profiler-combined-report` (depend on
`native-profiler-*`)
- `paste` — unregistered on both platforms for now.

---------

Co-authored-by: Paweł Fornagiel <pawel.fornagiel2@gmail.com>
2026-05-11 11:39:10 +02:00

90 lines
3.2 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
adaptNativeDescribeElementToDescribeNode,
adaptNativeDescribeToDescribeResult,
mapNativeTraitsToDescribeRole,
} from "../src/tools/describe/platforms/ios/ios-native-adapter";
import type { NativeDescribeScreenResult } from "../src/tools/native-devtools/native-describe-contract";
describe("describe native adapter", () => {
it("maps native traits to public describe roles", () => {
expect(mapNativeTraitsToDescribeRole(["header"])).toBe("AXHeading");
expect(mapNativeTraitsToDescribeRole(["button"])).toBe("AXButton");
expect(mapNativeTraitsToDescribeRole(["toggleButton"])).toBe("AXButton");
expect(mapNativeTraitsToDescribeRole(["staticText"])).toBe("AXStaticText");
expect(mapNativeTraitsToDescribeRole([])).toBe("AXGroup");
});
it("converts a native element into a leaf describe node", () => {
const node = adaptNativeDescribeElementToDescribeNode({
frame: { x: 16, y: 80, width: 100, height: 44 },
tapPoint: { x: 66, y: 102 },
normalizedFrame: { x: 0.1, y: 0.2, width: 0.3, height: 0.1 },
normalizedTapPoint: { x: 0.25, y: 0.25 },
traits: ["button"],
label: "Continue",
identifier: "continue-button",
});
expect(node).toEqual({
role: "AXButton",
frame: { x: 0.1, y: 0.2, width: 0.3, height: 0.1 },
children: [],
label: "Continue",
identifier: "continue-button",
value: undefined,
});
});
it("clamps partially off-screen normalized frames into the public [0,1] contract", () => {
const node = adaptNativeDescribeElementToDescribeNode({
frame: { x: -10, y: 800, width: 420, height: 100 },
tapPoint: { x: 200, y: 850 },
normalizedFrame: { x: -0.1, y: 0.95, width: 1.2, height: 0.2 },
normalizedTapPoint: { x: 0.5, y: 1.02 },
traits: ["button"],
});
expect(node?.frame).toEqual({
x: 0,
y: 0.95,
width: 1,
height: 0.05,
});
});
it("drops elements that have no visible normalized area after clamping", () => {
const node = adaptNativeDescribeElementToDescribeNode({
frame: { x: 0, y: 0, width: 10, height: 10 },
tapPoint: { x: 5, y: 5 },
normalizedFrame: { x: 1.2, y: 1.1, width: 0.2, height: 0.2 },
normalizedTapPoint: { x: 1.25, y: 1.2 },
traits: ["button"],
});
expect(node).toBeNull();
});
it("wraps native elements in the old recursive describe root shape", () => {
const result = adaptNativeDescribeToDescribeResult({
screenFrame: { x: 0, y: 0, width: 390, height: 844 },
elements: [
{
frame: { x: 16, y: 80, width: 100, height: 44 },
tapPoint: { x: 66, y: 102 },
normalizedFrame: { x: 0.1, y: 0.2, width: 0.3, height: 0.1 },
normalizedTapPoint: { x: 0.25, y: 0.25 },
traits: ["button"],
label: "Continue",
},
],
} satisfies NativeDescribeScreenResult);
expect(result.role).toBe("AXGroup");
expect(result.frame).toEqual({ x: 0, y: 0, width: 1, height: 1 });
expect(result.children).toHaveLength(1);
expect(result.children[0]?.role).toBe("AXButton");
expect(result.children[0]?.children).toEqual([]);
});
});