mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
f288a461b0
## Summary The Electron support added in #247 is **generic Chromium/CDP under the hood**. The discovery layer probes the Chromium-default CDP port (`9222`) and any `ARGENT_*_PORTS` entries; the connection picks the first non-devtools page target; describe is a plain in-page DOM walker; input/screenshot/screencast/navigation/JS-debugger are all straight CDP. Nothing in that path is Electron-specific — the only Electron-specific piece is the *launcher* (`boot-electron.ts`, which resolves and spawns an Electron binary). So any Chromium-family runtime exposing a CDP endpoint — an Electron app, **or** Chrome/Brave/Edge/WebView2 launched with `--remote-debugging-port` — is discoverable and drivable through the same tool surface. This PR renames the platform discriminator (and everything keyed off it) from `electron` to `chromium` so the naming matches reality. ## What changed **Public/contract surface (clean hard rename, no back-compat aliases):** - Platform discriminator `"electron"` → `"chromium"`; device id `electron-cdp-<port>` → `chromium-cdp-<port>`. - `ToolCapability.electron` → `.chromium`; the capability-gate rejection now reads `Tool 'X' is not supported on chromium app`. - Env `ARGENT_ELECTRON_PORTS`(`_FILE`) → `ARGENT_CHROMIUM_PORTS`(`_FILE`); ports cache `~/.argent/electron-cdp-ports.json` → `chromium-cdp-ports.json`; HTTP surface `/electron-server/` → `/chromium-server/`. **Files / dirs / symbols:** - `src/electron-server/` → `src/chromium-server/`; blueprints `electron-cdp` / `electron-js-runtime-debugger` → `chromium-*`; `describe`/`open-url`/`launch-app` `platforms/electron.ts` → `chromium.ts`; `keyboard/electron-keys` → `chromium-keys`; `utils/electron-discovery` → `chromium-discovery`. - `ElectronServer`/`ElectronCdpApi`/`ElectronJsRuntimeDebugger`/`describeElectron`/`discoverElectronDevices`/`ELECTRON_ID_PREFIX`/… → `Chromium*` / `CHROMIUM_*`. Tests renamed to match. **Stays "electron" (the Electron-specific launcher):** - `boot-device` keeps the `electronAppPath` / `electronPort` / `electronArgs` inputs, and `boot-electron.ts` still resolves & spawns an Electron binary. It just yields a `chromium` device now (an Electron app *is* a Chromium/CDP runtime). **Skills:** `argent-device-interact`, `argent-metro-debugger`, `rules/argent.md`, and the RN-workflow reference were reframed to describe **Chromium (CDP)** availability — Electron apps *plus* any Chromium browser over CDP (auto-discovered on `9222` / `ARGENT_CHROMIUM_PORTS`) — instead of Electron only. ## Test plan - [x] `tsc --build` clean - [x] `typecheck:tests` clean - [x] **1116 vitest cases pass (126 files)** - [x] prettier clean on all changed/renamed files - [x] Full-repo sweep: every remaining "electron" mention is intentional (the launcher path + skill prose explaining Electron as one kind of Chromium/CDP app); `electronic` in LICENSE/COC untouched.
145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { TypedEventEmitter } from "@argent/registry";
|
|
import { FpsTracker } from "../src/chromium-server/fps";
|
|
import { ScreencastManager } from "../src/chromium-server/screencast";
|
|
import type { CDPClient } from "../src/utils/debugger/cdp-client";
|
|
import type { ScreencastFrame, ServerEvents } from "../src/chromium-server/types";
|
|
|
|
interface FakeCdp {
|
|
send: ReturnType<typeof vi.fn>;
|
|
events: TypedEventEmitter<{ event: (method: string, params: Record<string, unknown>) => void }>;
|
|
emitFrame(frame: Partial<ScreencastFrame> & { sessionId: number; data: string }): void;
|
|
}
|
|
|
|
function makeFakeCdp(): FakeCdp {
|
|
const send = vi.fn().mockResolvedValue({});
|
|
const events = new TypedEventEmitter<{
|
|
event: (method: string, params: Record<string, unknown>) => void;
|
|
}>();
|
|
return {
|
|
send,
|
|
events,
|
|
emitFrame(frame) {
|
|
events.emit("event", "Page.screencastFrame", {
|
|
sessionId: frame.sessionId,
|
|
data: frame.data,
|
|
metadata: frame.metadata ?? {
|
|
offsetTop: 0,
|
|
pageScaleFactor: 1,
|
|
deviceWidth: 800,
|
|
deviceHeight: 600,
|
|
scrollOffsetX: 0,
|
|
scrollOffsetY: 0,
|
|
},
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("chromium-server/screencast", () => {
|
|
it("starts CDP screencast on the first subscriber and stops on the last", async () => {
|
|
const cdp = makeFakeCdp();
|
|
const events = new TypedEventEmitter<ServerEvents>();
|
|
const fps = new FpsTracker(events);
|
|
const mgr = new ScreencastManager(cdp as unknown as CDPClient, events, fps);
|
|
|
|
const s1 = await mgr.start({ format: "jpeg", quality: 60 });
|
|
expect(cdp.send).toHaveBeenCalledWith("Page.startScreencast", {
|
|
format: "jpeg",
|
|
quality: 60,
|
|
everyNthFrame: 1,
|
|
});
|
|
|
|
// Second subscriber shares the session — no second startScreencast.
|
|
cdp.send.mockClear();
|
|
const s2 = await mgr.start({ format: "jpeg", quality: 60 });
|
|
expect(cdp.send).not.toHaveBeenCalled();
|
|
|
|
// First stop doesn't tear down — second subscriber still active.
|
|
await s1.stop();
|
|
expect(cdp.send).not.toHaveBeenCalled();
|
|
|
|
// Second stop tears down.
|
|
await s2.stop();
|
|
expect(cdp.send).toHaveBeenCalledWith("Page.stopScreencast");
|
|
});
|
|
|
|
it("emits a 'frame' event and acks every frame so CDP keeps streaming", async () => {
|
|
const cdp = makeFakeCdp();
|
|
const events = new TypedEventEmitter<ServerEvents>();
|
|
const fps = new FpsTracker(events);
|
|
const mgr = new ScreencastManager(cdp as unknown as CDPClient, events, fps);
|
|
|
|
const frames: ScreencastFrame[] = [];
|
|
events.on("frame", (f) => frames.push(f));
|
|
await mgr.start();
|
|
|
|
cdp.emitFrame({ sessionId: 1, data: "AAA" });
|
|
cdp.emitFrame({ sessionId: 2, data: "BBB" });
|
|
|
|
expect(frames).toHaveLength(2);
|
|
expect(frames[0]?.data).toBe("AAA");
|
|
expect(frames[1]?.sessionId).toBe(2);
|
|
|
|
// Acks: we expect at least one ack per sessionId received.
|
|
const ackCalls = cdp.send.mock.calls.filter((c) => c[0] === "Page.screencastFrameAck");
|
|
expect(ackCalls.length).toBe(2);
|
|
expect((ackCalls[0]?.[1] as { sessionId: number }).sessionId).toBe(1);
|
|
expect((ackCalls[1]?.[1] as { sessionId: number }).sessionId).toBe(2);
|
|
|
|
expect(mgr.getLastFrame()?.data).toBe("BBB");
|
|
});
|
|
|
|
it("idempotent stop(): calling twice is safe", async () => {
|
|
const cdp = makeFakeCdp();
|
|
const events = new TypedEventEmitter<ServerEvents>();
|
|
const fps = new FpsTracker(events);
|
|
const mgr = new ScreencastManager(cdp as unknown as CDPClient, events, fps);
|
|
const s = await mgr.start();
|
|
await s.stop();
|
|
await s.stop(); // no-op the second time
|
|
const stops = cdp.send.mock.calls.filter((c) => c[0] === "Page.stopScreencast");
|
|
expect(stops.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("chromium-server/fps", () => {
|
|
it("emits fpsReport once per second when enabled", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const events = new TypedEventEmitter<ServerEvents>();
|
|
const reports: Array<{ fps: number }> = [];
|
|
events.on("fpsReport", (r) => reports.push(r));
|
|
const tracker = new FpsTracker(events);
|
|
tracker.setEnabled(true);
|
|
|
|
tracker.recordFrame();
|
|
tracker.recordFrame();
|
|
tracker.recordFrame();
|
|
vi.advanceTimersByTime(1000);
|
|
expect(reports[0]).toEqual({ fps: 3, windowMs: 1000 });
|
|
|
|
// Second window resets the counter.
|
|
tracker.recordFrame();
|
|
vi.advanceTimersByTime(1000);
|
|
expect(reports[1]).toEqual({ fps: 1, windowMs: 1000 });
|
|
|
|
tracker.setEnabled(false);
|
|
tracker.recordFrame();
|
|
vi.advanceTimersByTime(1000);
|
|
// No new report after disabling.
|
|
expect(reports).toHaveLength(2);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("setEnabled is idempotent", () => {
|
|
const events = new TypedEventEmitter<ServerEvents>();
|
|
const tracker = new FpsTracker(events);
|
|
tracker.setEnabled(true);
|
|
tracker.setEnabled(true); // should not double-arm the interval
|
|
tracker.dispose();
|
|
});
|
|
});
|