mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
d017ba340c
### 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>
194 lines
5.5 KiB
TypeScript
194 lines
5.5 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
adaptAXElement,
|
|
adaptAXDescribeToDescribeResult,
|
|
} from "../src/tools/describe/platforms/ios/ios-ax-adapter";
|
|
import type { AXDescribeResponse } from "../src/blueprints/ax-service";
|
|
|
|
describe("describe ax-service adapter", () => {
|
|
it("maps button trait to AXButton role", () => {
|
|
const node = adaptAXElement({
|
|
label: "Allow Once",
|
|
frame: { x: 0.1, y: 0.5, width: 0.3, height: 0.05 },
|
|
traits: ["button"],
|
|
});
|
|
expect(node?.role).toBe("AXButton");
|
|
});
|
|
|
|
it("maps header trait to AXHeading role", () => {
|
|
const node = adaptAXElement({
|
|
label: "General",
|
|
frame: { x: 0.05, y: 0.3, width: 0.9, height: 0.04 },
|
|
traits: ["header"],
|
|
});
|
|
expect(node?.role).toBe("AXHeading");
|
|
});
|
|
|
|
it("maps staticText trait to AXStaticText role", () => {
|
|
const node = adaptAXElement({
|
|
label: "Hello",
|
|
frame: { x: 0.1, y: 0.1, width: 0.5, height: 0.03 },
|
|
traits: ["staticText"],
|
|
});
|
|
expect(node?.role).toBe("AXStaticText");
|
|
});
|
|
|
|
it("defaults to AXGroup for empty traits", () => {
|
|
const node = adaptAXElement({
|
|
label: "Container",
|
|
frame: { x: 0.0, y: 0.0, width: 1.0, height: 1.0 },
|
|
traits: [],
|
|
});
|
|
expect(node?.role).toBe("AXGroup");
|
|
});
|
|
|
|
it("defaults to AXGroup when traits is undefined", () => {
|
|
const node = adaptAXElement({
|
|
label: "Container",
|
|
frame: { x: 0.0, y: 0.0, width: 1.0, height: 1.0 },
|
|
});
|
|
expect(node?.role).toBe("AXGroup");
|
|
});
|
|
|
|
it("clamps frames that extend beyond [0,1]", () => {
|
|
const node = adaptAXElement({
|
|
label: "Overflow",
|
|
frame: { x: -0.1, y: 0.95, width: 1.2, height: 0.2 },
|
|
traits: ["button"],
|
|
});
|
|
expect(node?.frame).toEqual({
|
|
x: 0,
|
|
y: 0.95,
|
|
width: 1,
|
|
height: 0.05,
|
|
});
|
|
});
|
|
|
|
it("drops elements with zero-size frames after clamping", () => {
|
|
const node = adaptAXElement({
|
|
label: "Offscreen",
|
|
frame: { x: 1.5, y: 1.5, width: 0.1, height: 0.1 },
|
|
traits: ["button"],
|
|
});
|
|
expect(node).toBeNull();
|
|
});
|
|
|
|
it("drops elements without a frame", () => {
|
|
const node = adaptAXElement({
|
|
label: "No frame",
|
|
traits: ["button"],
|
|
});
|
|
expect(node).toBeNull();
|
|
});
|
|
|
|
it("preserves label and value", () => {
|
|
const node = adaptAXElement({
|
|
label: "Search",
|
|
value: "query text",
|
|
frame: { x: 0.05, y: 0.1, width: 0.9, height: 0.04 },
|
|
traits: ["searchField"],
|
|
});
|
|
expect(node?.label).toBe("Search");
|
|
expect(node?.value).toBe("query text");
|
|
expect(node?.role).toBe("AXTextField");
|
|
});
|
|
|
|
it("produces nodes without a label (label is optional)", () => {
|
|
const node = adaptAXElement({
|
|
frame: { x: 0.1, y: 0.2, width: 0.3, height: 0.1 },
|
|
traits: ["image"],
|
|
});
|
|
expect(node).not.toBeNull();
|
|
expect(node?.role).toBe("AXImage");
|
|
expect(node?.label).toBeUndefined();
|
|
});
|
|
|
|
it("wraps elements in a root AXGroup", () => {
|
|
const response: AXDescribeResponse = {
|
|
alertVisible: false,
|
|
screenFrame: { width: 440, height: 956 },
|
|
elements: [
|
|
{
|
|
label: "General",
|
|
frame: { x: 0.045, y: 0.337, width: 0.909, height: 0.046 },
|
|
traits: ["button", "staticText"],
|
|
},
|
|
{
|
|
label: "Search",
|
|
frame: { x: 0.045, y: 0.16, width: 0.909, height: 0.038 },
|
|
value: "Search",
|
|
},
|
|
],
|
|
};
|
|
|
|
const root = adaptAXDescribeToDescribeResult(response);
|
|
expect(root.role).toBe("AXGroup");
|
|
expect(root.frame).toEqual({ x: 0, y: 0, width: 1, height: 1 });
|
|
expect(root.children).toHaveLength(2);
|
|
expect(root.children[0]?.label).toBe("General");
|
|
expect(root.children[1]?.label).toBe("Search");
|
|
});
|
|
|
|
it("produces an empty root for an empty elements array", () => {
|
|
const response: AXDescribeResponse = {
|
|
alertVisible: false,
|
|
elements: [],
|
|
};
|
|
|
|
const root = adaptAXDescribeToDescribeResult(response);
|
|
expect(root.role).toBe("AXGroup");
|
|
expect(root.children).toHaveLength(0);
|
|
});
|
|
|
|
it("handles an alertVisible response with dialog elements", () => {
|
|
const response: AXDescribeResponse = {
|
|
alertVisible: true,
|
|
screenFrame: { width: 440, height: 956 },
|
|
elements: [
|
|
{
|
|
label: "Allow Once",
|
|
frame: { x: 0.1, y: 0.5, width: 0.8, height: 0.05 },
|
|
traits: ["button"],
|
|
},
|
|
{
|
|
label: "Don\u2019t Allow",
|
|
frame: { x: 0.1, y: 0.56, width: 0.8, height: 0.05 },
|
|
traits: ["button"],
|
|
},
|
|
],
|
|
};
|
|
|
|
const root = adaptAXDescribeToDescribeResult(response);
|
|
expect(root.children).toHaveLength(2);
|
|
expect(root.children[0]?.role).toBe("AXButton");
|
|
expect(root.children[0]?.label).toBe("Allow Once");
|
|
expect(root.children[1]?.label).toBe("Don\u2019t Allow");
|
|
});
|
|
|
|
it("filters out elements with no visible area", () => {
|
|
const response: AXDescribeResponse = {
|
|
alertVisible: false,
|
|
elements: [
|
|
{
|
|
label: "Visible",
|
|
frame: { x: 0.1, y: 0.2, width: 0.3, height: 0.1 },
|
|
traits: ["button"],
|
|
},
|
|
{
|
|
label: "Zero width",
|
|
frame: { x: 0.5, y: 0.5, width: 0, height: 0.1 },
|
|
traits: ["button"],
|
|
},
|
|
{
|
|
label: "No frame",
|
|
traits: ["staticText"],
|
|
},
|
|
],
|
|
};
|
|
|
|
const root = adaptAXDescribeToDescribeResult(response);
|
|
expect(root.children).toHaveLength(1);
|
|
expect(root.children[0]?.label).toBe("Visible");
|
|
});
|
|
});
|