mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
7400179b5b
## What Makes Argent run on Windows. iOS Simulator is macOS-only (Apple), so on Windows that means the **Android + Chromium** host control plane. The blocker was twofold: (1) no Windows `simulator-server` binary, and (2) POSIX-only assumptions in the tool-server. Companion radon PR builds the binary and runs the Android emulator E2E on Windows: software-mansion/radon#134. ## Changes **Windows-aware tool-server / resolver / bundling** - New shared `commandOnPath()` helper — `where` on Windows, `command -v` via `/bin/sh` on POSIX. `android-binary`, `check-deps`, and `vega-cli` previously hardcoded `/bin/sh`, which never matches on Windows. - `android-binary`: append `.exe` to `adb`/`emulator` in the `$ANDROID_HOME` fallback; probe `%LOCALAPPDATA%\Android\Sdk` (Android Studio's Windows default). - simulator-server resolver + dispatcher + `bundle-tools` + `download-simulator-server.sh`: ship and resolve `simulator-server.exe` on `win32` (new `simulatorServerBinaryName()`), add `win32` to the bundled host keys, add the `simulator-server-argent-windows.exe` download target with PE arch validation. - `stop-metro`: resolve listening PIDs via `netstat -ano` on Windows (`lsof` is POSIX-only and threw ENOENT). - `adb.ts`: AVD root resolution used `$HOME` + `startsWith("/")` (rejects `C:\…`); now `os.homedir()` + `path.isAbsolute()`. - **`boot-device`**: the emulator was spawned `detached` with `stdio:"ignore"`; on Windows a detached emulator with NUL stdout/stderr never reaches `sys.boot_completed` (verified on `windows-latest`: the identical flag set boots fine with real handles, hangs with NUL). Redirect the detached emulator's output to a temp log file on win32. POSIX keeps `"ignore"`. **Tests** — cross-platform unit coverage for `commandOnPath` (both branches), win32 `.exe` Android resolution, win32 `.exe` simulator-server resolution, and a win32 AVD drive-path case; dep-gate tests now mock at the `commandOnPath` boundary so they're platform-agnostic. **CI** — `windows-e2e.yml` (`windows-chromium` on `windows-latest`): runs the resolver/dep unit tests natively on Windows, confirms `adb.exe` resolution via the SDK root, then drives a **real headless Chrome over CDP** through the tool-server (discover → screenshot → describe → tap → observe the DOM mutation). Flow in `scripts/ci/windows-chromium-e2e.mjs` (also runnable locally / in Docker). The `simulator-server.exe` build + the full **Android-emulator E2E on Windows** live in radon CI (the binary's source is the private radon repo). ## Verification (all green) - **Chromium control plane on real Windows** (`windows-latest`): `✓ discovered chromium-cdp-9222 → ✓ screenshot → ✓ describe → ✓ gesture-tap → ✓ DOM mutation observed`. Resolver/dep unit tests run natively; `adb.exe` resolves via the SDK root (it's *not* on PATH on the runner — the gap the resolver closes). - **Android emulator on real Windows** (radon#134, `windows-latest` / WHPX): the real Argent path — `boot-device` → `{ booted: true }` → screenshot via `simulator-server.exe` → gesture-tap — all green. (The boot-device fix above is what makes the detached emulator reach `boot_completed`.) - **Local clean-room E2E**: the Chromium flow runs green in a headless Linux Docker container (fresh `npm ci`, no host pollution). - **No regressions**: full tool-server suite (1624) + native-devtools-ios (16); ESLint/Prettier/typecheck clean; **Wayland E2E** (Linux AVD boot) and **Vega VVD E2E** still pass (they exercise the changed `adb.ts` / `boot-device.ts` / `vega-cli.ts` paths). ## Notes Android-emulator E2E needs WHPX nested virtualization — confirmed available on the current hosted `windows-latest` image (Server 2025); the emulator boots hardware-accelerated in ~3 min. --------- Co-authored-by: Ignacy Łątka <claude-hubert.gancarczyk@swmansion.com> Co-authored-by: Ignacy Łątka <latekvo@users.noreply.github.com>
139 lines
5.6 KiB
TypeScript
139 lines
5.6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
// launch-app / restart-app classify a tvOS sim as platform "ios" by UDID shape.
|
|
// native-devtools is iOS *and* tvOS capable — its ensureEnv injects the
|
|
// platform-matched DYLD_INSERT_LIBRARIES slice (the TVOSSIMULATOR bootstrap for
|
|
// Apple TV) — so the service is resolved for both kinds of sim. These tests pin
|
|
// that lazy-resolution behaviour: native-devtools is resolved regardless of
|
|
// whether the target is a regular iOS sim or a tvOS one.
|
|
|
|
const execFileMock = vi.fn(
|
|
(
|
|
cmd: string,
|
|
args: readonly string[],
|
|
opts: unknown,
|
|
cb?: (err: Error | null, out: { stdout: string; stderr: string }) => void
|
|
) => {
|
|
const callback = typeof opts === "function" ? opts : cb!;
|
|
// A host-binary dependency probe (`command -v <name>` via /bin/sh, or
|
|
// `where <name>` on Windows) resolves through `commandOnPath`, which treats
|
|
// EMPTY stdout as "not on PATH". So a probe must echo a path for the dep to
|
|
// count as available; other execFile calls (simctl launch/terminate) don't
|
|
// consume stdout in these tests.
|
|
const isProbe =
|
|
(cmd === "/bin/sh" && typeof args[1] === "string" && args[1].startsWith("command -v ")) ||
|
|
cmd === "where";
|
|
callback(null, { stdout: isProbe ? "/usr/bin/probe\n" : "", stderr: "" });
|
|
}
|
|
);
|
|
|
|
vi.mock("node:child_process", async () => {
|
|
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
|
|
return { ...actual, execFile: (...a: unknown[]) => (execFileMock as any)(...a) };
|
|
});
|
|
|
|
// Default to the iOS path; the tvOS cases override per-test.
|
|
vi.mock("../src/utils/ios-devices", () => ({
|
|
isTvOsSimulator: vi.fn(async () => false),
|
|
}));
|
|
|
|
// precheckNativeDevtools would otherwise probe a real api; stub it to a no-op
|
|
// "not blocked" so the handler proceeds to the simctl launch.
|
|
vi.mock("../src/blueprints/native-devtools", async () => {
|
|
const actual = await vi.importActual<typeof import("../src/blueprints/native-devtools")>(
|
|
"../src/blueprints/native-devtools"
|
|
);
|
|
return { ...actual, precheckNativeDevtools: vi.fn(async () => null) };
|
|
});
|
|
|
|
import { createLaunchAppTool } from "../src/tools/launch-app";
|
|
import { createRestartAppTool } from "../src/tools/restart-app";
|
|
import { isTvOsSimulator } from "../src/utils/ios-devices";
|
|
import { precheckNativeDevtools } from "../src/blueprints/native-devtools";
|
|
|
|
const mockIsTvOs = vi.mocked(isTvOsSimulator);
|
|
const mockPrecheck = vi.mocked(precheckNativeDevtools);
|
|
|
|
const IOS_UDID = "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA";
|
|
const TVOS_UDID = "DDDDDDDD-DDDD-DDDD-DDDD-DDDDDDDDDDDD";
|
|
|
|
function makeRegistry() {
|
|
const resolveService = vi.fn(async () => ({}) as unknown);
|
|
return { registry: { resolveService } as any, resolveService };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
execFileMock.mockClear();
|
|
mockIsTvOs.mockReset().mockResolvedValue(false);
|
|
mockPrecheck.mockClear().mockResolvedValue(null);
|
|
});
|
|
|
|
describe("launch-app — resolves native-devtools for iOS and tvOS", () => {
|
|
it("resolves native-devtools for a regular iOS simulator", async () => {
|
|
const { registry, resolveService } = makeRegistry();
|
|
const tool = createLaunchAppTool(registry);
|
|
|
|
const res = await tool.execute!({}, { udid: IOS_UDID, bundleId: "com.example.app" });
|
|
|
|
expect(res).toEqual({ launched: true, bundleId: "com.example.app" });
|
|
expect(resolveService).toHaveBeenCalledTimes(1);
|
|
expect(mockPrecheck).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("also resolves native-devtools for a tvOS simulator (platform-matched dylib)", async () => {
|
|
mockIsTvOs.mockResolvedValue(true);
|
|
const { registry, resolveService } = makeRegistry();
|
|
const tool = createLaunchAppTool(registry);
|
|
|
|
const res = await tool.execute!({}, { udid: TVOS_UDID, bundleId: "com.example.tvapp" });
|
|
|
|
expect(res).toEqual({ launched: true, bundleId: "com.example.tvapp" });
|
|
// ensureEnv picks the TVOSSIMULATOR slice, so injection is resolved on tvOS too.
|
|
expect(resolveService).toHaveBeenCalledTimes(1);
|
|
expect(mockPrecheck).toHaveBeenCalledTimes(1);
|
|
// It still launched the app via simctl.
|
|
const launched = execFileMock.mock.calls.some(
|
|
([cmd, args]) =>
|
|
cmd === "xcrun" &&
|
|
Array.isArray(args) &&
|
|
args.includes("launch") &&
|
|
args.includes(TVOS_UDID)
|
|
);
|
|
expect(launched).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("restart-app — resolves native-devtools for iOS and tvOS", () => {
|
|
it("resolves native-devtools for a regular iOS simulator", async () => {
|
|
const { registry, resolveService } = makeRegistry();
|
|
const tool = createRestartAppTool(registry);
|
|
|
|
const res = await tool.execute!({}, { udid: IOS_UDID, bundleId: "com.example.app" });
|
|
|
|
expect(res).toEqual({ restarted: true, bundleId: "com.example.app" });
|
|
expect(resolveService).toHaveBeenCalledTimes(1);
|
|
expect(mockPrecheck).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("also resolves native-devtools for a tvOS simulator (platform-matched dylib)", async () => {
|
|
mockIsTvOs.mockResolvedValue(true);
|
|
const { registry, resolveService } = makeRegistry();
|
|
const tool = createRestartAppTool(registry);
|
|
|
|
const res = await tool.execute!({}, { udid: TVOS_UDID, bundleId: "com.example.tvapp" });
|
|
|
|
expect(res).toEqual({ restarted: true, bundleId: "com.example.tvapp" });
|
|
expect(resolveService).toHaveBeenCalledTimes(1);
|
|
expect(mockPrecheck).toHaveBeenCalledTimes(1);
|
|
// terminate + launch still ran against the tvOS udid.
|
|
const launched = execFileMock.mock.calls.some(
|
|
([cmd, args]) =>
|
|
cmd === "xcrun" &&
|
|
Array.isArray(args) &&
|
|
args.includes("launch") &&
|
|
args.includes(TVOS_UDID)
|
|
);
|
|
expect(launched).toBe(true);
|
|
});
|
|
});
|