Files
software-mansion__argent/packages/tool-server/test/native-target-app.test.ts
filip131311 d9dc17d0d5 fix: move describe to native devtools (#83)
* fix: move describe to native devtools

Replace the AX describe path with an app-scoped native hierarchy adapter so coordinate discovery stays on the supported injection flow.
Also add safer targeting and restart guidance to reduce ambiguous Home and cold-start recovery paths.

Made-with: Cursor

* init

* update sim server

* Delete NATIVE_DEVTOOLS_DESCRIBE_MIGRATION_NOTE.md
2026-04-07 17:36:56 +02:00

149 lines
5.1 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
chooseFrontmostConnectedApp,
inspectConnectedNativeApps,
resolveNativeTargetApp,
} from "../src/utils/native-target-app";
import type { NativeAppState, NativeDevtoolsApi } from "../src/blueprints/native-devtools";
function makeAppState(bundleId: string, overrides: Partial<NativeAppState> = {}): NativeAppState {
return {
bundleId,
applicationState: "background",
foregroundActiveSceneCount: 0,
foregroundInactiveSceneCount: 0,
backgroundSceneCount: 1,
unattachedSceneCount: 0,
isFrontmostCandidate: false,
...overrides,
};
}
function makeApi(apps: NativeAppState[]): NativeDevtoolsApi {
const byBundleId = new Map(apps.map((app) => [app.bundleId, app]));
return {
isEnvSetup: () => true,
socketPath: "/tmp/mock.sock",
ensureEnvReady: async () => {},
isConnected: (bundleId) => byBundleId.has(bundleId),
isAppRunning: async (bundleId) => byBundleId.has(bundleId),
listConnectedBundleIds: () => [...byBundleId.keys()],
requiresAppRestart: async () => false,
activateNetworkInspection: () => {},
getNetworkLog: () => [],
clearNetworkLog: () => {},
getAppState: async (bundleId) => {
const app = byBundleId.get(bundleId);
if (!app) throw new Error(`unknown bundleId: ${bundleId}`);
return app;
},
detectFrontmostBundleId: async () => null,
queryViewHierarchy: async () => ({}),
};
}
describe("native-target-app", () => {
describe("chooseFrontmostConnectedApp", () => {
it("returns the unique strong candidate when one app is active", () => {
const result = chooseFrontmostConnectedApp([
makeAppState("com.example.a", { applicationState: "background" }),
makeAppState("com.example.b", { applicationState: "active" }),
]);
expect(result?.bundleId).toBe("com.example.b");
});
it("returns the unique weak candidate when no strong candidate exists", () => {
const result = chooseFrontmostConnectedApp([
makeAppState("com.example.a", { applicationState: "background" }),
makeAppState("com.example.b", {
applicationState: "inactive",
foregroundInactiveSceneCount: 1,
}),
]);
expect(result?.bundleId).toBe("com.example.b");
});
it("returns null when multiple strong candidates exist", () => {
const result = chooseFrontmostConnectedApp([
makeAppState("com.example.a", { applicationState: "active" }),
makeAppState("com.example.b", { foregroundActiveSceneCount: 1 }),
]);
expect(result).toBeNull();
});
});
describe("inspectConnectedNativeApps", () => {
it("returns connected apps sorted by bundle id", async () => {
const api = makeApi([makeAppState("com.example.z"), makeAppState("com.example.a")]);
const result = await inspectConnectedNativeApps(api);
expect(result.map((app) => app.bundleId)).toEqual(["com.example.a", "com.example.z"]);
});
});
describe("resolveNativeTargetApp", () => {
it("uses explicit bundleId when provided", async () => {
const api = makeApi([makeAppState("com.example.a")]);
await expect(resolveNativeTargetApp(api, "com.example.explicit")).resolves.toEqual({
bundleId: "com.example.explicit",
source: "explicit",
});
});
it("auto-selects the only connected app", async () => {
const api = makeApi([
makeAppState("com.example.only", {
applicationState: "active",
foregroundActiveSceneCount: 1,
}),
]);
await expect(resolveNativeTargetApp(api)).resolves.toEqual({
bundleId: "com.example.only",
source: "single_connected_foreground_like",
});
});
it("requires explicit bundleId when the only connected app is background-only", async () => {
const api = makeApi([makeAppState("com.example.background-only")]);
await expect(resolveNativeTargetApp(api)).rejects.toThrow(
"Provide bundleId explicitly if you still want to target this app."
);
});
it("auto-selects the unique frontmost connected app", async () => {
const api = makeApi([
makeAppState("com.example.background"),
makeAppState("com.example.front", { applicationState: "active" }),
]);
await expect(resolveNativeTargetApp(api)).resolves.toEqual({
bundleId: "com.example.front",
source: "frontmost_detected",
});
});
it("throws when no connected apps are available", async () => {
const api = makeApi([]);
await expect(resolveNativeTargetApp(api)).rejects.toThrow(
"No native-devtools-connected apps are available for auto-targeting. Launch or restart the app first, provide bundleId explicitly, or use screenshot to inspect visible Home/system UI."
);
});
it("throws when multiple connected apps exist and none is uniquely frontmost", async () => {
const api = makeApi([
makeAppState("com.example.a", { applicationState: "active" }),
makeAppState("com.example.b", { foregroundActiveSceneCount: 1 }),
]);
await expect(resolveNativeTargetApp(api)).rejects.toThrow("Provide bundleId explicitly.");
});
});
});