// Integration tests: real Vite sockets + a fake cargo process tree, no GUI/build. // Run: node --test frontend/scripts/unix-dev.test.mjs import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import { once } from "node:events"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { createServer } from "node:net"; import os from "node:os"; import path from "node:path"; import { setTimeout as delay } from "node:timers/promises"; import { test } from "node:test"; import WebSocket from "ws"; import { fileURLToPath } from "node:url"; const rootDir = fileURLToPath(new URL("../../", import.meta.url)); const launcher = path.join(rootDir, "script/artcraft/unix_dev.sh"); test("skips occupied ports, pairs Tauri with real Vite/HMR, and cleans up on Ctrl-C", { timeout: 40000 }, async (t) => { const occupied = await occupyPort(t); const startingPort = occupied.address().port; const run = await fixture(t, { ARTCRAFT_DEV_PORT: String(startingPort) }); const record = await run.record(); const config = overrides(record); const url = config.build.devUrl; assert.ok(Number(new URL(url).port) > startingPort); assert.equal(new URL(url).hostname, "127.0.0.1"); assert.equal(config.build.beforeDevCommand, ""); assert.ok(!record.args.includes("--no-watch")); assert.ok(!record.args.includes("--no-dev-server-wait")); assert.equal(record.env.TAURI_APP_PATH, path.join(rootDir, "crates/desktop/artcraft")); assert.equal(record.env.TAURI_FRONTEND_PATH, path.join(rootDir, "frontend")); assert.ok(config.app.security.devCsp["connect-src"].includes(url.replace("http:", "ws:"))); assert.match(await (await fetch(url)).text(), /\/src\/index\.tsx/); assert.match(await (await fetch(`${url}/@vite/client`)).text(), /WebSocket/); run.proc.kill("SIGINT"); assert.deepEqual(await run.exited, [130, null], run.output()); await until(() => !alive(record.pid) && !alive(record.childPid)); await assert.rejects(fetch(url)); assert.equal(occupied.listening, true, "must not terminate the existing listener"); }); test("Tauri failure propagates and stops Vite and leftover descendants", { timeout: 40000 }, async (t) => { const run = await fixture(t, { TEST_EXIT_CODE: "17" }); const record = await run.record(); assert.deepEqual(await run.exited, [17, null], run.output()); await until(() => !alive(record.childPid)); await assert.rejects(fetch(overrides(record).build.devUrl)); }); test("SIGTERM shuts down the complete process tree", { timeout: 40000 }, async (t) => { const run = await fixture(t); const record = await run.record(); run.proc.kill("SIGTERM"); assert.deepEqual(await run.exited, [143, null], run.output()); await until(() => !alive(record.pid) && !alive(record.childPid)); await assert.rejects(fetch(overrides(record).build.devUrl)); }); test("invalid ports fail before starting Tauri", { timeout: 40000 }, async (t) => { const run = await fixture(t, { ARTCRAFT_DEV_PORT: "65536" }); assert.deepEqual(await run.exited, [1, null]); assert.match(run.output(), /ARTCRAFT_DEV_PORT must be an integer/); }); test("concurrent launchers use different ports without replacing either frontend", { timeout: 40000 }, async (t) => { const occupied = await occupyPort(t); const env = { ARTCRAFT_DEV_PORT: String(occupied.address().port) }; const first = await fixture(t, env); const second = await fixture(t, env); const [a, b] = await Promise.all([first.record(), second.record()]); const firstUrl = overrides(a).build.devUrl; const secondUrl = overrides(b).build.devUrl; assert.notEqual(firstUrl, secondUrl); assert.equal((await fetch(firstUrl)).status, 200); assert.equal((await fetch(secondUrl)).status, 200); first.proc.kill("SIGINT"); await first.exited; assert.equal((await fetch(secondUrl)).status, 200); assert.ok(alive(b.pid)); }); test("a React source edit sends HMR on the selected port without restarting Rust", { timeout: 40000 }, async (t) => { const run = await fixture(t); const record = await run.record(); const url = overrides(record).build.devUrl; const probeDir = await mkdtemp(path.join(rootDir, "frontend/apps/artcraft/app/src/dev-probe-")); t.after(() => rm(probeDir, { recursive: true, force: true })); const probe = path.join(probeDir, "Probe.tsx"); const moduleUrl = `/src/${path.basename(probeDir)}/Probe.tsx`; await writeFile(probe, "export default function Probe() { return