Skip to content

Commit 67d5f7d

Browse files
committed
[scramjet/runway] improve harness, add inspect command
1 parent 85577fd commit 67d5f7d

File tree

6 files changed

+199
-5
lines changed

6 files changed

+199
-5
lines changed

packages/scramjet/packages/runway/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"type": "module",
77
"scripts": {
88
"test": "node --experimental-strip-types --no-warnings src/index.ts",
9-
"test:headed": "HEADED=1 node --experimental-strip-types --no-warnings src/index.ts"
9+
"test:headed": "HEADED=1 node --experimental-strip-types --no-warnings src/index.ts",
10+
"inspect": "node --experimental-strip-types --no-warnings src/inspect.ts"
1011
},
1112
"dependencies": {
1213
"@mercuryworkshop/scramjet": "workspace:*",

packages/scramjet/packages/runway/src/harness/scramjet/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import express from "express";
22
import path from "path";
33
import { fileURLToPath } from "url";
44
import http from "http";
5-
import { server as wisp } from "@mercuryworkshop/wisp-js/server";
5+
import { server as wisp, logging } from "@mercuryworkshop/wisp-js/server";
66

77
const __dirname = path.dirname(fileURLToPath(import.meta.url));
88

@@ -54,6 +54,7 @@ export async function startHarness() {
5454
});
5555
wisp.options.allow_private_ips = true;
5656
wisp.options.allow_loopback_ips = true;
57+
logging.set_level(logging.NONE);
5758

5859
wispServer.on("upgrade", (req, socket, head) => {
5960
wisp.routeRequest(req, socket, head);

packages/scramjet/packages/runway/src/harness/scramjet/public/index.html

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
<body>
1010
<h1>Scramjet Test Harness</h1>
1111
<div id="status">Initializing...</div>
12+
<button id="reload-btn" style="display: none; margin: 8px 0">
13+
🔄 Reload Test
14+
</button>
1215
<iframe
1316
id="testframe"
1417
style="width: 100%; height: 80vh; border: 1px solid #ccc"
@@ -17,6 +20,7 @@ <h1>Scramjet Test Harness</h1>
1720
<script>
1821
const statusEl = document.getElementById("status");
1922
const testframe = document.getElementById("testframe");
23+
const reloadBtn = document.getElementById("reload-btn");
2024
const { Controller } = $scramjetController;
2125
const LibcurlClient = window.LibcurlTransport.LibcurlClient;
2226

@@ -37,6 +41,8 @@ <h1>Scramjet Test Harness</h1>
3741
window.__testFail(message, details);
3842
}
3943
};
44+
iframeWin.__topEval = window.eval;
45+
iframeWin.__eval = iframeWin.eval;
4046
}
4147
} catch (e) {
4248
console.warn("Could not inject test functions:", e);
@@ -48,7 +54,7 @@ <h1>Scramjet Test Harness</h1>
4854
// temporary hack, contextInit is coming soon ish
4955
let observer = null;
5056
function setupIframeObserver() {
51-
setInterval(injectTestFunctions, 50);
57+
setInterval(injectTestFunctions, 0);
5258
if (testframe.contentDocument) {
5359
observer = new MutationObserver(() => {
5460
injectTestFunctions();
@@ -113,10 +119,29 @@ <h1>Scramjet Test Harness</h1>
113119

114120
window.__runwayNavigate = (url) => {
115121
statusEl.textContent = `Loading test: ${url}`;
122+
// Store the URL for reload support
123+
sessionStorage.setItem("__runway_last_test", url);
124+
reloadBtn.style.display = "inline-block";
116125
injectTestFunctions();
117126
scramjetFrame.go(url);
118127
};
119128

129+
window.__runwayReload = () => {
130+
const lastTest = sessionStorage.getItem("__runway_last_test");
131+
if (lastTest) {
132+
window.__runwayNavigate(lastTest);
133+
}
134+
};
135+
136+
reloadBtn.addEventListener("click", window.__runwayReload);
137+
138+
// Auto-navigate to last test on reload
139+
const lastTest = sessionStorage.getItem("__runway_last_test");
140+
if (lastTest) {
141+
console.log("Reloading last test:", lastTest);
142+
window.__runwayNavigate(lastTest);
143+
}
144+
120145
console.log("Harness ready, __runwayNavigate exposed");
121146
}
122147

packages/scramjet/packages/runway/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ async function discoverTests(): Promise<Test[]> {
2929
const fullPath = path.join(__dirname, "tests", file);
3030
const module = await import(fullPath);
3131
if (module.default) {
32-
tests.push(module.default);
32+
// Handle both single test and array of tests
33+
if (Array.isArray(module.default)) {
34+
tests.push(...module.default);
35+
} else {
36+
tests.push(module.default);
37+
}
3338
}
3439
}
3540
return tests;
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { chromium } from "playwright";
2+
import type { Test } from "./testcommon.ts";
3+
import { glob } from "node:fs/promises";
4+
import path from "node:path";
5+
import { fileURLToPath } from "node:url";
6+
7+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
8+
9+
async function discoverTests(): Promise<Test[]> {
10+
const testFiles = glob("**/*.ts", {
11+
cwd: path.join(__dirname, "tests"),
12+
});
13+
14+
const tests: Test[] = [];
15+
for await (const file of testFiles) {
16+
const fullPath = path.join(__dirname, "tests", file);
17+
const module = await import(fullPath);
18+
if (module.default) {
19+
if (Array.isArray(module.default)) {
20+
tests.push(...module.default);
21+
} else {
22+
tests.push(module.default);
23+
}
24+
}
25+
}
26+
return tests;
27+
}
28+
29+
async function main() {
30+
const testFilter = process.argv[2];
31+
32+
if (!testFilter) {
33+
console.log("Usage: pnpm inspect <test-name-pattern>");
34+
console.log("\nAvailable tests:");
35+
const tests = await discoverTests();
36+
for (const test of tests) {
37+
console.log(` - ${test.name}`);
38+
}
39+
process.exit(1);
40+
}
41+
42+
console.log(`🔍 Inspecting tests matching: ${testFilter}\n`);
43+
44+
// Discover and filter tests
45+
const allTests = await discoverTests();
46+
const tests = allTests.filter((t) => t.name.includes(testFilter));
47+
48+
if (tests.length === 0) {
49+
console.log(`❌ No tests found matching "${testFilter}"`);
50+
console.log("\nAvailable tests:");
51+
for (const test of allTests) {
52+
console.log(` - ${test.name}`);
53+
}
54+
process.exit(1);
55+
}
56+
57+
console.log(`📋 Found ${tests.length} matching test(s):`);
58+
for (const test of tests) {
59+
console.log(` - ${test.name}`);
60+
}
61+
console.log();
62+
63+
// Start all matching tests
64+
for (const test of tests) {
65+
await test.start();
66+
console.log(
67+
`🌐 Test "${test.name}" running at http://localhost:${test.port}/`
68+
);
69+
}
70+
71+
// Start the harness server
72+
const { startHarness, PORT: HARNESS_PORT } = await import(
73+
"./harness/scramjet/index.ts"
74+
);
75+
await startHarness();
76+
const harnessUrl = `http://localhost:${HARNESS_PORT}`;
77+
console.log(`\n📡 Harness running at ${harnessUrl}`);
78+
79+
// Launch browser
80+
const browser = await chromium.launch({
81+
headless: false,
82+
devtools: true,
83+
});
84+
85+
const page = await browser.newPage();
86+
87+
// Expose test reporting functions
88+
await page.exposeFunction("__testPass", (message?: string, details?: any) => {
89+
console.log(`\n✅ Test passed${message ? `: ${message}` : ""}`);
90+
if (details) console.log(" Details:", details);
91+
});
92+
93+
await page.exposeFunction("__testFail", (message?: string, details?: any) => {
94+
console.log(`\n❌ Test failed${message ? `: ${message}` : ""}`);
95+
if (details) console.log(" Details:", details);
96+
});
97+
98+
page.on("pageerror", (error) => {
99+
console.log(`\n💥 Page error: ${error.message}`);
100+
});
101+
102+
page.on("console", (msg) => {
103+
const type = msg.type();
104+
const prefix = type === "error" ? "❌" : type === "warning" ? "⚠️" : "📝";
105+
console.log(`${prefix} [console.${type}] ${msg.text()}`);
106+
});
107+
108+
// Navigate to harness
109+
await page.goto(harnessUrl);
110+
111+
// Wait for harness to be ready
112+
try {
113+
await page.waitForFunction(
114+
() => typeof (window as any).__runwayNavigate === "function",
115+
{ timeout: 30000 }
116+
);
117+
console.log("✅ Harness ready\n");
118+
} catch (e) {
119+
console.error("💥 Harness failed to initialize");
120+
await browser.close();
121+
process.exit(1);
122+
}
123+
124+
// Navigate to first test
125+
const firstTest = tests[0];
126+
const testUrl = `http://localhost:${firstTest.port}/`;
127+
console.log(`🚀 Navigating to test: ${testUrl}`);
128+
129+
await page.evaluate((url) => {
130+
(window as any).__runwayNavigate(url);
131+
}, testUrl);
132+
133+
console.log("\n" + "─".repeat(50));
134+
console.log("🔍 Browser open for manual inspection");
135+
console.log(" Press Ctrl+C to exit\n");
136+
137+
if (tests.length > 1) {
138+
console.log("Other test URLs (navigate manually via harness):");
139+
for (const test of tests.slice(1)) {
140+
console.log(` - http://localhost:${test.port}/`);
141+
}
142+
console.log();
143+
}
144+
145+
// Keep the process running
146+
await new Promise(() => {});
147+
}
148+
149+
main().catch((err) => {
150+
console.error("Fatal error:", err);
151+
process.exit(1);
152+
});

packages/scramjet/packages/runway/src/testcommon.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ function fail(message, details) {
3838
__testFail(message, details);
3939
}
4040
41+
const realtop = __eval("top");
42+
const reallocation = __eval("location");
43+
const realparent = __eval("location");
44+
function checkglobal(global) {
45+
assert(global !== realtop, "top was leaked");
46+
assert(global !== reallocation, "location was leaked");
47+
assert(global !== realparent, "parent was leaked");
48+
assert(global !== __eval, "eval was leaked");
49+
}
50+
4151
function assert(condition, message) {
4252
if (!condition) {
4353
fail(message || 'Assertion failed');
@@ -103,7 +113,7 @@ export function basicTest(props: { name: string; js: string }): Test {
103113
res.end(COMMON_JS);
104114
} else if (req.url === "/script.js") {
105115
res.writeHead(200, { "Content-Type": "application/javascript" });
106-
res.end(props.js);
116+
res.end(`runTest(async () => {\n${props.js}\n});`);
107117
} else {
108118
res.writeHead(404);
109119
res.end("Not found");

0 commit comments

Comments
 (0)