repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/mockup/__tests__/mockup.test.ts
Rawimport assert from "node:assert";
import { request as httpRequest } from "node:http";
import { describe, it } from "vitest";
import {
MOCKUP_FULL_DOCUMENT_ERROR,
type MockupRequest,
renderMockupPage,
startMockupServer,
validateMockupRequest,
} from "../mockup.ts";
import {
previewDeviceScaleFactor,
renderMockupPreviewPage,
thinReplayFrames,
} from "../mockup-preview.ts";
import { openerCandidates } from "../src/pi-ext-browser-open.ts";
const client = {
userAgent: "mockup-test-browser/1",
viewport: { width: 1280, height: 720, devicePixelRatio: 1 },
};
const baseRequest: MockupRequest = {
title: "Pick layout",
question: "Which option works?",
options: [
{
id: "a",
label: "A",
description: "First direction",
html: "<section><h3>A</h3><script>window.allowed = true;</script></section>",
},
],
};
describe("mockup validation", () => {
it("rejects full HTML documents with an agent-facing fix", () => {
for (const html of [
"<!doctype html><html></html>",
"<!-- built elsewhere --><html><body>full</body></html>",
]) {
assert.throws(
() =>
validateMockupRequest({
...baseRequest,
options: [{ id: "full", label: "Full", html }],
}),
(error) => {
assert.ok(error instanceof Error);
assert.strictEqual(
error.message,
MOCKUP_FULL_DOCUMENT_ERROR("options[0].html"),
);
return true;
},
);
}
});
it("allows scripts and styles inside HTML fragments", () => {
const request = validateMockupRequest({
...baseRequest,
options: [
{
...baseRequest.options[0],
html: '<section><style>.x::before{content:"<html"}</style><script>const tag = "<body";</script></section>',
},
],
});
assert.match(
request.options[0].html,
/<style>\.x::before\{content:"<html"\}<\/style>/,
);
assert.match(
request.options[0].html,
/<script>const tag = "<body";<\/script>/,
);
});
it("rejects parameters outside the minimal public schema", () => {
assert.throws(
() => validateMockupRequest({ ...baseRequest, timeout_ms: 1000 }),
/timeout_ms is not a supported mockup parameter/,
);
});
});
describe("mockup page", () => {
it("matches capture pixels to the terminal image dimensions", () => {
const scale = previewDeviceScaleFactor(1, 174, {
widthPx: 11,
heightPx: 26,
});
assert.ok(Math.abs(scale - 880 / 680) < Number.EPSILON);
});
it("bounds replay memory while preserving the cycle endpoints", () => {
const frames = Array.from({ length: 9 }, (_, capturedAt) => ({
capturedAt,
frames: ["x".repeat(10)],
}));
const retained = thinReplayFrames(frames, 4, 40);
assert.ok(retained.length <= 4);
assert.ok(
retained.reduce((total, frame) => total + frame.frames[0].length, 0) <=
40,
);
assert.strictEqual(retained[0], frames[0]);
assert.strictEqual(retained.at(-1), frames.at(-1));
});
it("renders a capture-only preview without interactive review controls", () => {
const html = renderMockupPreviewPage(baseRequest);
assert.match(html, /class="preview-card"/);
assert.match(html, /class="fragment-content"/);
assert.match(html, /attachShadow/);
assert.match(html, /mockupRoot/);
assert.match(html, /background:transparent/);
assert.match(html, /\\u003csection>/);
assert.doesNotMatch(html, /id="accept"|id="iterate"|api\/events/);
});
it("renders fragments inside selectable cards with checkbox controls", () => {
const html = renderMockupPage(baseRequest, "tok");
assert.match(html, /type="checkbox"/);
assert.match(html, /data-option-id="a"/);
assert.match(
html,
/\\u003csection>\\u003ch3>A\\u003c\/h3>\\u003cscript>window\.allowed = true;\\u003c\/script>\\u003c\/section>/,
);
assert.match(html, /Pin note/);
assert.match(html, /pin-bubble/);
assert.match(html, /Delete pin/);
});
it("keeps fragment styles inert until shadow rendering", () => {
const html = renderMockupPage(
{
...baseRequest,
options: [
{
id: "bleed",
label: "Bleed",
html: "<style>.panel{position:absolute}</style><section>Card</section>",
},
],
},
"tok",
);
const fragmentMarkup = html.slice(
html.indexOf('<div class="fragment"'),
html.indexOf('<section class="drawer"'),
);
assert.match(fragmentMarkup, /data-fragment-content/);
assert.match(
fragmentMarkup,
/<script type="application\/json" data-fragment-html>/,
);
assert.doesNotMatch(fragmentMarkup, /<style>\.panel\{position:absolute/);
assert.match(fragmentMarkup, /\\u003cstyle>\.panel\{position:absolute/);
});
it("shows global notes and annotations as responsive split panes", () => {
const html = renderMockupPage(baseRequest, "tok");
const topbar = html.slice(
html.indexOf('<header class="topbar">'),
html.indexOf('<main class="stage"'),
);
const drawer = html.slice(html.indexOf('<section class="drawer"'));
assert.doesNotMatch(topbar, /id="iterate"|id="accept"/);
assert.match(drawer, /id="iterate"/);
assert.match(drawer, /id="accept"/);
assert.doesNotMatch(drawer, /role="tablist"|data-tab=/);
assert.ok(
drawer.indexOf('id="global-notes"') <
drawer.indexOf('id="annotations-panel"'),
);
assert.match(html, /\.feedback-grid \{[^}]*grid-template-columns:minmax/);
assert.match(html, /@media \(max-width:720px\)/);
assert.match(html, /\.feedback-grid \{ grid-template-columns:1fr; \}/);
});
it("shows one-shot pin mode with cursor and toggle state", () => {
const html = renderMockupPage(baseRequest, "tok");
assert.match(
html,
/\.option-card\.pin-armed \.fragment \{[^}]*cursor:crosshair/,
);
assert.match(html, /card\.classList\.toggle\("pin-armed", armed === id\)/);
assert.match(
html,
/pin\.textContent = armed === id \? "Pinning…" : "Pin note"/,
);
assert.match(html, /armed = null;\n {2}sync\(\);\n {2}showBubble/);
});
it("selects label-backed options only once", () => {
const html = renderMockupPage(baseRequest, "tok");
assert.match(html, /\$\("input", card\)\.addEventListener\("change"/);
assert.match(html, /closest\("label,button"\)/);
});
it("keeps annotations inside the nearest fragment edge", () => {
const html = renderMockupPage(baseRequest, "tok");
assert.match(html, /if \(a\.x > 0\.5\) bubble\.style\.right/);
assert.match(html, /else bubble\.style\.left/);
assert.match(html, /button\.style\.left = "clamp\(14px,/);
});
it("places pins relative to scrolled fragment content", () => {
const html = renderMockupPage(baseRequest, "tok");
assert.match(
html,
/\(event\.clientX - rect\.left \+ frag\.scrollLeft\) \/ frag\.scrollWidth/,
);
assert.match(
html,
/\(event\.clientY - rect\.top \+ frag\.scrollTop\) \/ frag\.scrollHeight/,
);
});
});
describe("mockup browser opener", () => {
it("uses the native URL handler directly on win32 without cmd", () => {
assert.deepStrictEqual(
openerCandidates("http://127.0.0.1:1", {
platform: "win32",
env: {},
})[0],
{
command: "rundll32.exe",
args: ["url.dll,FileProtocolHandler", "http://127.0.0.1:1"],
windowsHide: true,
},
);
});
});
describe("mockup server", () => {
it("rejects multiple submitted selections unless multi is enabled", async () => {
const server = await startMockupServer(
{
...baseRequest,
options: [
...baseRequest.options,
{ id: "b", label: "B", html: "<section>B</section>" },
],
},
{ openBrowser: false },
);
try {
const res = await fetch(new URL("/api/submit", server.url), {
method: "POST",
headers: {
Authorization: `Bearer ${server.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
action: "accept",
selected: ["a", "b"],
notes: "",
annotations: [],
client,
}),
});
assert.strictEqual(res.status, 400);
assert.match(await res.text(), /selected must contain at most one item/);
} finally {
server.result.catch(() => undefined);
server.close("test");
}
});
it("preserves UTF-8 split across request chunks", async () => {
const server = await startMockupServer(baseRequest, { openBrowser: false });
try {
const body = Buffer.from(
JSON.stringify({
action: "accept",
selected: ["a"],
notes: "before 😀 after",
annotations: [],
client,
}),
);
const split = body.indexOf(Buffer.from("😀")) + 1;
await new Promise<void>((resolve, reject) => {
const url = new URL("/api/submit", server.url);
const req = httpRequest(
url,
{
method: "POST",
headers: {
Authorization: `Bearer ${server.token}`,
"Content-Type": "application/json",
},
},
(res) => {
res.resume();
res.once("end", resolve);
},
);
req.once("error", reject);
req.write(body.subarray(0, split));
setImmediate(() => req.end(body.subarray(split)));
});
assert.strictEqual((await server.result).notes, "before 😀 after");
} finally {
server.result.catch(() => undefined);
server.close("test");
}
});
it("blocks until browser submits action, selection, notes, and annotations", async () => {
const server = await startMockupServer(baseRequest, { openBrowser: false });
try {
assert.match(server.url, /[?&]k=/);
assert.doesNotMatch(server.url, /token=|key=/);
const posted = fetch(new URL("/api/submit", server.url), {
method: "POST",
headers: {
Authorization: `Bearer ${server.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
action: "iterate",
selected: ["a"],
notes: "keep A, denser drawer",
annotations: [{ optionId: "a", x: 0.25, y: 0.5, note: "tighten" }],
client,
}),
});
const res = await posted;
assert.strictEqual(await res.text(), '{"ok":true}');
const result = await server.result;
assert.deepStrictEqual(
{
action: result.action,
selected: result.selected,
notes: result.notes,
annotations: result.annotations,
userAgent: result.review.userAgent,
viewport: result.review.viewport,
},
{
action: "iterate",
selected: ["a"],
notes: "keep A, denser drawer",
annotations: [{ optionId: "a", x: 0.25, y: 0.5, note: "tighten" }],
userAgent: client.userAgent,
viewport: client.viewport,
},
);
assert.match(result.review.submittedAt, /^\d{4}-\d{2}-\d{2}T/);
} finally {
server.result.catch(() => undefined);
server.close("test");
}
});
it("keeps one URL across iterations and accepts no selection", async () => {
const server = await startMockupServer(baseRequest, { openBrowser: false });
try {
const first = server.result;
const firstResponse = await fetch(new URL("/api/submit", server.url), {
method: "POST",
headers: {
Authorization: `Bearer ${server.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
action: "iterate",
selected: ["a"],
notes: "try again",
annotations: [],
client,
}),
});
assert.strictEqual(firstResponse.status, 200);
assert.strictEqual((await first).action, "iterate");
const stableUrl = server.url;
const second = server.present({
...baseRequest,
question: "Is this revision right?",
});
assert.strictEqual(server.url, stableUrl);
const page = await fetch(server.url);
assert.match(await page.text(), /Is this revision right\?/);
const secondResponse = await fetch(new URL("/api/submit", server.url), {
method: "POST",
headers: {
Authorization: `Bearer ${server.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
action: "accept",
selected: [],
notes: "none works",
annotations: [],
client,
}),
});
assert.strictEqual(secondResponse.status, 200);
assert.deepStrictEqual((await second).selected, []);
} finally {
server.result.catch(() => undefined);
server.close("test");
}
});
it("authenticates browser requests that stop active Pi work", async () => {
let aborts = 0;
const server = await startMockupServer(baseRequest, {
openBrowser: false,
abortAgent: () => {
aborts++;
return true;
},
});
try {
const unauthorized = await fetch(new URL("/api/abort", server.url), {
method: "POST",
});
assert.strictEqual(unauthorized.status, 401);
const stopped = await fetch(new URL("/api/abort", server.url), {
method: "POST",
headers: { Authorization: `Bearer ${server.token}` },
});
assert.strictEqual(stopped.status, 200);
assert.strictEqual(aborts, 1);
} finally {
server.result.catch(() => undefined);
server.close("test");
}
});
});