Something went wrong. Try again.
Something went wrong. Try again.
6.8 kB · 126 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127import { afterEach, describe, expect, it } from "vitest";import { readFileSync, readdirSync, writeFileSync } from "node:fs";import { createServer } from "node:net";import { tmpdir } from "node:os";import { join, resolve } from "node:path";import { runCli } from "../../runner/cli";import { realRunDeps, runScriptFile, type RunDeps, type RunOptions } from "../../runner/run";import { createJsdomBrowser } from "../helpers/jsdomBrowser";import { startExtension } from "../helpers/extensionRuntime";import { HELLO, makeProject, page, type Project } from "../helpers/project";import { makeTempRepo } from "../helpers/tempRepo";
/** * The whole of `aite run` in this process: real script and config files, the real fixture server over real HTTP, the real bridge * over a real WebSocket, the extension's real code, and the runner's engine. Only Chrome itself is replaced, by jsdom running the * pages. (The same scripts run in a real Chrome in run.e2e.test.ts.) */
const root = resolve(__dirname, "../..");const projects: Project[] = [];const browsers: Array<ReturnType<typeof createJsdomBrowser>> = [];afterEach(() => { for (const p of projects.splice(0)) p.repo.cleanup(); browsers.splice(0);});
/** Real files, real fixture server and bridge; nothing here gets as far as a page, so there is no browser at all. */function deps(over: Partial<RunDeps> = {}): RunDeps { return { ...realRunDeps(), launch: async () => ({ close: async () => undefined, exited: new Promise(() => undefined), stderrTail: () => "" }), ...over };}
const options = (p: Pick<Project, "scriptPath" | "configPath">, over: Partial<RunOptions> = {}): RunOptions => ({ scriptPath: p.scriptPath, configPath: p.configPath, headless: true, noSandbox: true, connectTimeoutMs: 5000, ...over });
async function project(pages: Record<string, string>, script: (origin: string) => string) { const p = await makeProject(pages, script); projects.push(p); return p;}const run = (p: Project, over: Partial<RunOptions> = {}, d: RunDeps = deps()) => runScriptFile(options(p, over), d);
const clickHello = (o: string, verify: string, timeout = "1s") => `test "t"\n start ${o}/hello.html\n timeout ${timeout}\n step "click"\n click role:button name:"Say hello"\n step "greeting"\n verify text:${JSON.stringify(verify)}\n`;
describe("cannot run (exit 2) with real files", () => { it("a script file that does not exist", async () => { const p = await project({}, () => ""); const r = await run(p, { scriptPath: join(p.dir, "nope.aite") }); expect(r.exitCode).toBe(2); expect(r.output).toContain("cannot read"); }); it("a script with mistakes lists them with their lines", async () => { const p = await project({ "hello.html": HELLO }, (o) => `test "t"\n start ${o}/hello.html\n step\n clik text:"x"\n click\n`); const r = await run(p); expect(r.exitCode).toBe(2); expect(r.output).toMatch(/t\.aite:3: a step needs a name/); expect(r.output).toMatch(/t\.aite:4: unknown command "clik"/); expect(r.output).toMatch(/t\.aite:5: click needs a target/); expect(r.output).toContain("3 errors in"); }); it("a config file that does not exist, or is not valid", async () => { const p = await project({ "hello.html": HELLO }, (o) => clickHello(o, "Waiting")); expect((await run(p, { configPath: join(p.dir, "nope.json") })).output).toContain("cannot read"); writeFileSync(p.configPath, "{ nope"); const r = await run(p); expect(r.exitCode).toBe(2); expect(r.output).toContain("invalid aite config"); }); it("a start page not in the allow list, and a profile that is not configured", async () => { const p = await project({ "hello.html": HELLO }, (o) => clickHello(o, "Waiting")); writeFileSync(p.configPath, JSON.stringify({ profiles: { p: { userDataDir: join(p.dir, "profile"), chrome: "/unused" } }, allow: ["https://elsewhere.example"] })); expect((await run(p)).output).toContain("is not in the allow list"); expect((await run(p, { profile: "ghost" })).output).toContain('profile "ghost" is not in aite.config.json'); }); it("a fixture port that is already taken", async () => { const p = await project({ "hello.html": HELLO }, (o) => clickHello(o, "Waiting")); const blocker = await new Promise<ReturnType<typeof createServer>>((ok) => { const s = createServer().listen(p.port, "127.0.0.1", () => ok(s)); }); try { const r = await run(p); expect(r.exitCode).toBe(2); expect(r.output).toContain(`port ${p.port} is already in use`); } finally { blocker.close(); } });});
describe("environment failures (exit 3)", () => { it("Chrome that cannot be started", async () => { const p = await project({ "hello.html": HELLO }, (o) => clickHello(o, "Waiting")); writeFileSync(p.configPath, JSON.stringify({ profiles: { p: { userDataDir: join(p.dir, "profile"), chrome: "/nonexistent/chrome" } }, allow: [p.origin] })); const r = await runScriptFile(options(p), realRunDeps()); expect(r.exitCode).toBe(3); expect(r.output).toContain("cannot start Chrome at /nonexistent/chrome"); }); it("an extension that never connects: reports it after the wait, and tidies up", async () => { const p = await project({ "hello.html": HELLO }, (o) => clickHello(o, "Waiting")); const r = await run(p, { connectTimeoutMs: 300 }, deps({ launch: async () => ({ close: async () => undefined, exited: new Promise(() => undefined), stderrTail: () => "" }) })); expect(r.exitCode).toBe(3); expect(r.output).toContain("did not connect within 300 ms"); }); it("Chrome dying at startup is reported at once with its own message", async () => { const p = await project({ "hello.html": HELLO }, (o) => clickHello(o, "Waiting")); const t0 = Date.now(); const r = await run(p, { connectTimeoutMs: 20_000 }, deps({ launch: async () => ({ close: async () => undefined, exited: Promise.resolve({ code: null, signal: "SIGABRT" }), stderrTail: () => "[1:1:FATAL:zygote] No usable sandbox!" }) })); expect(Date.now() - t0).toBeLessThan(5000); expect(r.exitCode).toBe(3); expect(r.output).toContain("No usable sandbox!"); });});
describe("the folders a run creates", () => { it("are private to the user (mode 700): a Chrome profile holds signed-in sessions", async () => { const { mkdtempSync, statSync, rmSync } = await import("node:fs"); const { tmpdir } = await import("node:os"); const base = mkdtempSync(join(tmpdir(), "aite-mode-")); try { const dir = join(base, "a", "profile"); realRunDeps().makeDir(dir); expect(statSync(dir).mode & 0o777).toBe(0o700); } finally { rmSync(base, { recursive: true, force: true }); } });});