SPB Git

Repository Public Research preview

server.mjs 8.2 KB 101 lines javascript

1// Local fixture site for the Safari tests. Listens on 127.0.0.1 only; no external requests.2import { createHash, randomBytes } from "node:crypto";3import { createServer } from "node:http";45const page = (title, body, head = "") => `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title}</title>${head}</head><body>${body}</body></html>`;6export const REPORT = Buffer.alloc(300_000).map((_, i) => (i * 31 + 7) % 251);7export const REPORT_SHA = createHash("sha256").update(REPORT).digest("hex");89const PAGES = {10  "/": () => page("Fixture home", `11<h1>Fixture home</h1>12<nav><a href="/form.html">Open the form</a> · <a href="/table.html">Price table</a> · <a href="/about.html" target="_blank" id="newtab">About (new tab)</a> · <a href="/redirect">Redirect me</a> · <a href="/long.html">Long page</a></nav>13<h2>Delayed content</h2>14<button id="reveal">Reveal later</button><div id="later"></div>15<h2>Duplicates</h2>16<button class="dup">Save</button> <button class="dup">Save</button>17<h2>Dialogs</h2>18<button id="ask">Ask to confirm</button> <span id="answer">no answer</span>19<h2>Shadow DOM</h2><div id="host"></div>20<h2>Frames</h2><iframe src="/frame.html" title="same origin" width="300" height="80"></iframe>21<iframe src="http://localhost:__PORT__/frame.html" title="cross origin" width="300" height="80"></iframe>22<p id="hidden-note" style="display:none">This paragraph is hidden.</p>23<script>24document.getElementById("reveal").onclick = () => setTimeout(() => { document.getElementById("later").innerHTML = '<p id="arrived">The content arrived.</p>'; }, 2500);25document.getElementById("ask").onclick = () => { document.getElementById("answer").textContent = confirm("Proceed with the operation?") ? "confirmed" : "cancelled"; };26const root = document.getElementById("host").attachShadow({ mode: "open" });27root.innerHTML = '<button id="shadow-btn">Shadow button</button><span id="shadow-out"></span>';28root.getElementById("shadow-btn").onclick = () => { root.getElementById("shadow-out").textContent = "shadow clicked"; document.title = "Fixture home (shadow clicked)"; };29</script>`),30  "/frame.html": () => page("Frame", `<button id="frame-btn">Frame button</button><span id="frame-out"></span><script>document.getElementById("frame-btn").onclick=()=>{document.getElementById("frame-out").textContent="frame clicked";};</script>`),31  "/about.html": () => page("About the fixture", `<h1>About</h1><p>This page exists to be opened in a new tab.</p><a href="/">Home</a>`),32  "/slow.html": () => page("Slow page", `<h1>Slow page</h1><p>Served after a delay.</p>`),33  "/long.html": () => page("Long page", `<h1>Long page</h1>${Array.from({ length: 120 }, (_, i) => `<p>Paragraph number ${i + 1} of the long page.</p>`).join("")}<button id="bottom-btn">Bottom button</button><span id="bottom-out"></span><script>document.getElementById("bottom-btn").onclick=()=>{document.getElementById("bottom-out").textContent="bottom clicked";};</script>`),34  "/table.html": () => page("Price table", `<h1>Prices</h1><table><caption>Quarterly prices</caption><thead><tr><th>Item</th><th>Quarter</th><th>Price</th></tr></thead><tbody>${Array.from({ length: 30 }, (_, i) => `<tr><td>Item ${i + 1}</td><td>Q${(i % 4) + 1}</td><td>${(i + 1) * 10}.50</td></tr>`).join("")}</tbody></table>`),35  "/form.html": () => page("Fixture form", `36<h1>Registration</h1>37<form id="reg" method="post" action="/submit" enctype="multipart/form-data">38  <label for="name">Full name</label> <input id="name" name="name" required>39  <span>Live mirror: <output id="mirror"></output></span>40  <label>Email <input type="email" name="email" placeholder="you@example.org"></label>41  <label for="pw">Password</label> <input id="pw" type="password" name="password">42  <label for="country">Country</label>43  <select id="country" name="country"><option value="">Choose…</option><option value="ca">Canada</option><option value="fr">France</option><option value="jp">Japan</option></select>44  <label><input type="checkbox" name="terms" value="yes"> I accept the terms</label>45  <fieldset><legend>Plan</legend>46    <label><input type="radio" name="plan" value="free" checked> Free</label>47    <label><input type="radio" name="plan" value="pro"> Pro</label></fieldset>48  <label for="notes">Notes</label> <textarea id="notes" name="notes"></textarea>49  <div id="bio" contenteditable="true" aria-label="Biography"></div><input type="hidden" name="bio" id="bio-field">50  <label for="doc">Document</label> <input id="doc" type="file" name="doc"> <output id="doc-info"></output>51  <button type="submit" id="send">Create account</button>52</form>53<h2>Search</h2>54<form id="search" method="get" action="/search"><input name="q" aria-label="Search the site"></form>55<script>56// A "controlled" input in the style of UI frameworks: state only updates through input events.57const state = { name: "" };58const name = document.getElementById("name");59name.addEventListener("input", (e) => { state.name = e.target.value; document.getElementById("mirror").textContent = state.name; });60document.getElementById("reg").addEventListener("submit", () => { name.value = state.name; document.getElementById("bio-field").value = document.getElementById("bio").textContent; });61document.getElementById("doc").addEventListener("change", async (e) => {62  const f = e.target.files[0]; if (!f) return;63  const d = await crypto.subtle.digest("SHA-256", await f.arrayBuffer());64  document.getElementById("doc-info").textContent = f.name + " " + f.size + " " + [...new Uint8Array(d)].map((b) => b.toString(16).padStart(2, "0")).join("");65});66</script>`),67};6869export async function startFixtures() {70  // A fresh token per run: a cookie left in the browser by an earlier run must not count as signed in.71  const token = randomBytes(8).toString("hex");72  const signedIn = (req) => (req.headers.cookie ?? "").includes(`session=${token}`);73  const server = createServer(async (req, res) => {74    const url = new URL(req.url, "http://x");75    const send = (status, type, body, headers = {}) => { res.writeHead(status, { "Content-Type": type, "Cache-Control": "no-store", ...headers }); res.end(body); };76    if (url.pathname === "/redirect") return send(302, "text/plain", "", { Location: "/about.html" });77    if (url.pathname === "/login") return send(200, "text/html", page("Signed in", "<h1>Signed in</h1><p>The session cookie is set.</p>"), { "Set-Cookie": `session=${token}; Path=/; HttpOnly; SameSite=Lax` });78    if (url.pathname === "/download/report.bin") {79      if (!signedIn(req)) return send(403, "text/plain", "sign in first");80      return send(200, "application/octet-stream", REPORT, { "Content-Disposition": 'attachment; filename="report.bin"' });81    }82    if (url.pathname === "/api/data.json") return send(200, "application/json", JSON.stringify({ items: [1, 2, 3], signed_in: signedIn(req) }));83    if (url.pathname === "/search") return send(200, "text/html", page("Search results", `<h1>Results for <q id="q">${String(url.searchParams.get("q") ?? "").replace(/[<>&]/g, "")}</q></h1>`));84    if (url.pathname === "/submit" && req.method === "POST") {85      const chunks = []; for await (const c of req) chunks.push(c);86      const raw = Buffer.concat(chunks).toString("latin1");87      const fields = {};88      for (const m of raw.matchAll(/name="([^"]+)"(?:; filename="([^"]*)")?\r\n(?:Content-Type: [^\r\n]+\r\n)?\r\n([\s\S]*?)\r\n--/g)) fields[m[1]] = m[2] !== undefined ? `file:${m[2]}:${m[3].length}` : m[3];89      if (!raw.includes("form-data")) for (const [k, v] of new URLSearchParams(raw)) fields[k] = v;90      return send(200, "text/html", page("Account created", `<h1>Account created</h1><pre id="received">${JSON.stringify(fields).replace(/[<>&]/g, "")}</pre>`));91    }92    if (url.pathname === "/slow.html") await new Promise((r) => setTimeout(r, 1200));93    const p = PAGES[url.pathname === "/index.html" ? "/" : url.pathname];94    if (!p) return send(404, "text/html", page("Not found", "<h1>Not found</h1>"));95    send(200, "text/html; charset=utf-8", p().replaceAll("__PORT__", String(server.address().port)));96  });97  await new Promise((r) => server.listen(0, "127.0.0.1", r));98  const origin = `http://127.0.0.1:${server.address().port}`;99  return { origin, port: server.address().port, close: () => new Promise((r) => { server.closeAllConnections?.(); server.close(() => r()); }) };100}101
Files main
Menu

Theme