// Process runtime (VISION D2): a `web` process has web globals only and an ESM // entry; `node` (the default) keeps today's Node globals. import { test, expect, type Page } from "@playwright/test"; import { runGuest, type GuestResult } from "./guest"; const web = (page: Page, files: Record, entry = "/app/main.mjs"): Promise => runGuest(page, files, entry, "", { runtime: "web" }); test("web: no Node globals", async ({ page }) => { const out = await web(page, { "/app/main.mjs": "console.log(typeof process, typeof Buffer, typeof global, typeof setImmediate, typeof clearImmediate);", }); expect(out).toEqual({ code: 0, signal: null, stdout: "undefined undefined undefined undefined undefined\n", stderr: "" }); }); test("web: timers return numbers, keep the loop alive, and clear by number", async ({ page }) => { const out = await web(page, { "/app/main.mjs": [ "const cancelled = setTimeout(() => console.log('cancelled timeout ran'), 10);", "clearTimeout(cancelled);", "const iv = setInterval(() => console.log('cancelled interval ran'), 10);", "clearInterval(iv);", "console.log(typeof cancelled, typeof iv);", "setTimeout(() => console.log('fired'), 50);", "console.log('sync done');", ].join("\n"), }); expect(out).toEqual({ code: 0, signal: null, stdout: "number number\nsync done\nfired\n", stderr: "" }); }); test("web: node: builtins import from ESM", async ({ page }) => { const out = await web(page, { "/app/main.mjs": [ "import { readFileSync, writeFileSync } from 'node:fs';", "import fs from 'node:fs';", "import { join } from 'node:path';", "import { EventEmitter } from 'node:events';", "writeFileSync(join('/app', 'out.txt'), 'written');", "console.log(readFileSync('/app/out.txt', 'utf8'), fs.existsSync('/app/out.txt'));", "const e = new EventEmitter();", "e.on('ping', (v) => console.log('event', v));", "e.emit('ping', 7);", ].join("\n"), }); expect(out).toEqual({ code: 0, signal: null, stdout: "written true\nevent 7\n", stderr: "" }); }); test("web: a CJS entry is refused", async ({ page }) => { const out = await web(page, { "/app/main.cjs": "console.log('ran');" }, "/app/main.cjs"); expect(out.code).toBe(1); expect(out.stdout).toBe(""); expect(out.stderr).toContain("ERR_NIB_WEB_ENTRY_NOT_ESM"); expect(out.stderr).toContain("/app/main.cjs"); }); test("web: an uncaught throw prints its stack and exits 1", async ({ page }) => { const out = await web(page, { "/app/main.mjs": "console.log('before');\nthrow new Error('boom');" }); expect(out.code).toBe(1); expect(out.stdout).toBe("before\n"); expect(out.stderr).toMatch(/^Error: boom\n {4}at /); }); test("web: worker_threads threads inherit the runtime", async ({ page }) => { const out = await web(page, { "/app/main.mjs": [ "import { Worker } from 'node:worker_threads';", "const w = new Worker('/app/thread.mjs');", "w.on('message', (m) => console.log('thread sees', m));", "w.on('exit', (code) => console.log('thread exit', code));", ].join("\n"), "/app/thread.mjs": "import { parentPort } from 'node:worker_threads';\nparentPort.postMessage(typeof process);", }); expect(out).toEqual({ code: 0, signal: null, stdout: "thread sees undefined\nthread exit 0\n", stderr: "" }); }); test("web: an eval worker is refused, since its source is a CommonJS script", async ({ page }) => { const out = await web(page, { "/app/main.mjs": [ "import { Worker } from 'node:worker_threads';", "const w = new Worker('console.log(\"ran\")', { eval: true });", "w.on('error', (e) => console.log('thread error', e.message));", "w.on('exit', (code) => console.log('thread exit', code));", ].join("\n"), }); expect(out).toEqual({ code: 0, signal: null, stdout: "thread error A web process's entry must be an ES module; the eval source is a CommonJS script.\nthread exit 1\n", stderr: "", }); }); test("web: a node child is a node process", async ({ page }) => { const out = await web(page, { "/app/main.mjs": [ "import { spawn } from 'node:child_process';", "const child = spawn('node', ['/app/child.cjs']);", "child.stdout.on('data', (d) => console.log('child says', String(d).trim()));", "child.on('close', (code) => console.log('child exit', code));", ].join("\n"), "/app/child.cjs": "console.log(typeof process, typeof Buffer);", }); expect(out).toEqual({ code: 0, signal: null, stdout: "child says object function\nchild exit 0\n", stderr: "" }); }); test("node stays the default runtime", async ({ page }) => { const out = await runGuest(page, { "/app/main.mjs": "console.log(typeof process, typeof Buffer, typeof setImmediate, typeof setTimeout(() => {}, 0));", }, "/app/main.mjs"); expect(out).toEqual({ code: 0, signal: null, stdout: "object function function object\n", stderr: "" }); }); test("the worker scope belongs to the guest: its postMessage cannot reach the kernel", async ({ page }) => { const forged = "postMessage({ t: 'stdio', stream: 'stdout', bytes: new TextEncoder().encode('forged\\n') });"; for (const [runtime, entry] of [["web", "/app/main.mjs"], ["node", "/app/main.mjs"]] as const) { const out = await runGuest(page, { "/app/main.mjs": [ "let heard = 0;", "self.onmessage = () => { heard++; };", forged, "setTimeout(() => console.log('still talking to the kernel', heard), 20);", ].join("\n"), }, entry, "", { runtime }); expect(out, runtime).toEqual({ code: 0, signal: null, stdout: "still talking to the kernel 0\n", stderr: "" }); } }); test("web: close() ends the process after the current task, as it ends a worker", async ({ page }) => { const out = await web(page, { "/app/main.mjs": [ "setTimeout(() => console.log('queued timer ran'), 0);", "console.log('before close');", "close();", "console.log('rest of the task');", ].join("\n"), }); expect(out).toEqual({ code: 0, signal: null, stdout: "before close\nrest of the task\n", stderr: "" }); });