Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ target/
# the cutover, four rebuilds of an ~11 MB engine composite landed in
# history. `just pages` still writes here for local preview.
/docs/demo/
# Same class: spikes/todomvc's justfile writes its built demo here.
/docs/spike-todomvc/
1 change: 1 addition & 0 deletions spikes/todomvc/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
target/
build/
node_modules/
e2e/test-results/
363 changes: 302 additions & 61 deletions spikes/todomvc/README.md

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions spikes/todomvc/deno.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
{
"imports": {
"@deltic/runtime/embedder": "jsr:@deltic/runtime@0.1.0-pre.gc4043e6/embedder",
"@deltic/translator": "jsr:@deltic/translator@0.1.0-pre.gc4043e6"
"@deltic/runtime/embedder": "../../../polyengine-dioxus/.deps/polyengine/runtime/src/embedder/mod.ts",
"@deltic/runtime/shim": "../../../polyengine-dioxus/.deps/polyengine/runtime/src/shim/mod.ts",
"@deltic/protocol": "../../../polyengine-dioxus/.deps/polyengine/protocol/src/mod.ts",
"@deltic/translator": "../../../polyengine-dioxus/.deps/polyengine/translator/mod.ts",
"@polyengine/wasi": "../../../polyengine-dioxus/.deps/polyengine/wasi/src/mod.ts",
"@polyengine/dioxus-host/": "../../../polyengine-dioxus/host/src/",
"@playwright/test": "npm:@playwright/test@1.62.1"
},
"compilerOptions": {
"lib": ["deno.ns", "dom", "dom.iterable", "esnext"]
Expand Down
49 changes: 9 additions & 40 deletions spikes/todomvc/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions spikes/todomvc/e2e/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 75 additions & 0 deletions spikes/todomvc/e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Playwright global setup: starts e2e/server.ts (a Deno static file
// server) bound to port 0, parses the real port from its own stdout, and
// exposes it to tests via process.env.E2E_BASE_URL.
//
// Port/PID discipline (dispatch mandatory rules):
// - bind port 0, parse the real port from the server's own output —
// never hard-code a port (parallel worktrees collide silently).
// - kill by PID with a /proc/<pid>/cwd check inside THIS worktree in
// global-teardown.ts, never by port-pattern pkill.
//
// State (pid + expected cwd) is handed to teardown via a temp JSON file
// rather than process.env, since Playwright's global teardown runs in a
// fresh Node invocation in some configurations and env alone isn't
// guaranteed to survive that boundary.

import { spawn } from "node:child_process";
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = join(fileURLToPath(new URL(".", import.meta.url)), "..");
const serverScript = join(repoRoot, "e2e", "server.ts");

export interface E2EServerState {
pid: number;
expectedCwd: string;
baseUrl: string;
stateFile: string;
}

const STATE_ENV_VAR = "E2E_SERVER_STATE_FILE";

export default async function globalSetup(): Promise<void> {
const child = spawn("deno", ["run", "--allow-net", "--allow-read", serverScript], {
cwd: repoRoot,
stdio: ["ignore", "pipe", "inherit"],
});

const port = await new Promise<number>((resolve, reject) => {
let buf = "";
const onData = (chunk: Buffer) => {
buf += chunk.toString("utf8");
const m = buf.match(/^LISTENING (\d+)$/m);
if (m) {
child.stdout?.off("data", onData);
resolve(Number(m[1]));
}
};
child.stdout?.on("data", onData);
child.once("error", reject);
child.once("exit", (code) => reject(new Error(`e2e/server.ts exited early with code ${code}`)));
setTimeout(() => reject(new Error("timed out waiting for e2e/server.ts to report its port")), 15_000);
});

const stateDir = mkdtempSync(join(tmpdir(), "todomvc-spike-e2e-"));
const stateFile = join(stateDir, "server-state.json");
const state: E2EServerState = {
pid: child.pid!,
expectedCwd: repoRoot,
baseUrl: `http://localhost:${port}`,
stateFile,
};
writeFileSync(stateFile, JSON.stringify(state));

process.env.E2E_BASE_URL = state.baseUrl;
process.env[STATE_ENV_VAR] = stateFile;
// Detach from this process's stdio lifecycle but keep the reference for
// teardown's kill-by-PID step (teardown reads the pid back from
// stateFile, not from this in-memory handle, in case Playwright forks a
// separate process for teardown).
child.unref();
}

export { STATE_ENV_VAR };
41 changes: 41 additions & 0 deletions spikes/todomvc/e2e/global-teardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Playwright global teardown: kill the e2e/server.ts process started by
// global-setup.ts.
//
// Kill-by-PID discipline (dispatch mandatory rule): verify
// /proc/<pid>/cwd resolves inside this worktree before sending a signal —
// never kill by a port/pattern match, since the port may by now belong to
// an entirely different process (this repo's own conventions doc:
// ~/.config/opencode/AGENTS.md "Ad-hoc dev servers").

import { existsSync, readFileSync, readlinkSync } from "node:fs";
import type { E2EServerState } from "./global-setup.ts";

export default async function globalTeardown(): Promise<void> {
const stateFile = process.env.E2E_SERVER_STATE_FILE;
if (!stateFile || !existsSync(stateFile)) return;

const state = JSON.parse(readFileSync(stateFile, "utf8")) as E2EServerState;

const cwdLink = `/proc/${state.pid}/cwd`;
if (!existsSync(cwdLink)) return; // already gone

let actualCwd: string;
try {
actualCwd = readlinkSync(cwdLink);
} catch {
return; // process exited between the existsSync check and readlink
}

if (actualCwd !== state.expectedCwd) {
console.warn(
`e2e global-teardown: refusing to kill pid ${state.pid} — cwd is ${actualCwd}, expected ${state.expectedCwd} (not our server)`,
);
return;
}

try {
process.kill(state.pid, "SIGTERM");
} catch {
// already exited
}
}
78 changes: 78 additions & 0 deletions spikes/todomvc/e2e/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions spikes/todomvc/e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "todomvc-spike-e2e",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"test": "playwright test"
},
"devDependencies": {
"@playwright/test": "1.62.1"
}
}
44 changes: 44 additions & 0 deletions spikes/todomvc/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Playwright config for the TodoMVC spike's real-browser E2E lane. Chromium
// only. Setup, from a fresh checkout:
// cd e2e && npm install && npx playwright install chromium
//
// The spike had NO automated gate before this lane; these are its two.
//
// tests/harness.spec.ts — wraps the existing differential harness
// (web/harness.html), turning a manual check
// into a gate. This is what proves the runtime
// bump did not break the three surface guests.
// tests/dioxus-frame.spec.ts — the re-targeted dioxus guest on its default
// (frame) backend, driven through real
// interaction inside the sandboxed frame.
//
// Server: global-setup.ts spawns e2e/server.ts (a Deno static file server)
// bound to port 0 and exposes the resolved port via process.env.E2E_BASE_URL;
// global-teardown.ts kills it by PID with a /proc/<pid>/cwd check. Playwright's
// built-in `webServer` is not used because it has no first-class "parse the
// real port out of stdout" support.
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
testDir: "./tests",
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: 0,
workers: 1,
reporter: [["list"]],
globalSetup: "./global-setup.ts",
globalTeardown: "./global-teardown.ts",
// baseURL is deliberately NOT set: globalSetup resolves the server's
// ephemeral port, but this config object is evaluated BEFORE globalSetup
// runs, so a `use.baseURL` read here would freeze on `undefined`. Tests read
// process.env.E2E_BASE_URL directly.
use: {
trace: "retain-on-failure",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});
Loading
Loading