feat(chrome): hand snapshots to context mode

This commit is contained in:
云服务部-叶林立
2026-08-28 14:45:37 +08:00
parent 221e978622
commit f3a2abe1e4
96 changed files with 12800 additions and 26 deletions
@@ -0,0 +1,28 @@
<!doctype html>
<meta charset="utf-8">
<title>01 isTrusted click</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: click the green button. Page only accepts <code>event.isTrusted === true</code>.</p>
<button id="go" style="padding:20px 32px;font-size:18px;background:#1f7a1f;color:#fff;border:0;border-radius:6px">Click me</button>
</main>
<script>
Challenge.init({
id: "is-trusted-click",
instructions: "click the green button; isTrusted must be true",
});
const btn = document.getElementById("go");
btn.addEventListener("click", (e) => {
Challenge.log("click", { isTrusted: e.isTrusted, x: e.clientX, y: e.clientY });
if (e.isTrusted) Challenge.pass("click.isTrusted === true");
else Challenge.fail("click.isTrusted === false (synthetic dispatchEvent)");
}, { capture: true });
// Also watch pointerdown to detect bot earlier.
btn.addEventListener("pointerdown", (e) => {
Challenge.log("pointerdown", { isTrusted: e.isTrusted, pressure: e.pressure, pointerType: e.pointerType });
if (!e.isTrusted) Challenge.fail("pointerdown.isTrusted === false");
});
</script>
</body>
@@ -0,0 +1,50 @@
<!doctype html>
<meta charset="utf-8">
<title>02 isTrusted keyboard</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: type <code>hello</code> into the box. Each character must arrive via a trusted keydown/keypress/input.</p>
<input id="t" placeholder="type hello" style="font-size:18px;padding:8px 12px;width:240px">
</main>
<script>
Challenge.init({
id: "is-trusted-keyboard",
instructions: "type 'hello'; every keydown.isTrusted must be true",
});
const t = document.getElementById("t");
let trustedKeys = 0, untrustedKeys = 0, inputs = 0, untrustedInputs = 0;
t.addEventListener("keydown", (e) => {
Challenge.log("keydown", { key: e.key, isTrusted: e.isTrusted });
if (e.isTrusted) trustedKeys++; else untrustedKeys++;
});
t.addEventListener("input", (e) => {
Challenge.log("input", { isTrusted: e.isTrusted, inputType: e.inputType, data: e.data });
inputs++; if (!e.isTrusted) untrustedInputs++;
});
t.addEventListener("keyup", () => {
if (t.value === "hello") {
if (untrustedKeys || untrustedInputs) {
Challenge.fail(`untrusted keydowns=${untrustedKeys}, untrusted inputs=${untrustedInputs}`);
} else if (trustedKeys < 5) {
Challenge.fail(`only ${trustedKeys} trusted keydowns (need >=5)`);
} else {
Challenge.pass(`${trustedKeys} trusted keydowns, ${inputs} trusted inputs`);
}
}
});
// Programmatic value-set detection.
const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
Object.defineProperty(t, "value", {
get() { return desc.get.call(this); },
set(v) {
Challenge.log("value-set", { v });
if (untrustedKeys === 0 && trustedKeys === 0) {
Challenge.fail("value set programmatically without any keydown");
}
return desc.set.call(this, v);
},
});
</script>
</body>
@@ -0,0 +1,51 @@
<!doctype html>
<meta charset="utf-8">
<title>03 webdriver / runtime fingerprint</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Auto-checks runtime properties commonly inspected by bot-detection scripts.</p>
<pre id="rep" style="font:12px monospace;background:#111;color:#eee;padding:12px;border-radius:6px"></pre>
</main>
<script>
Challenge.init({
id: "webdriver-flag",
instructions: "no interaction; auto-verdict on load",
});
const checks = [];
function chk(name, bad, note) {
checks.push({ name, bad, note });
if (bad) Challenge.log("flag", { name, note });
}
chk("navigator.webdriver", navigator.webdriver === true, String(navigator.webdriver));
chk("languages-empty", !navigator.languages || navigator.languages.length === 0,
JSON.stringify(navigator.languages));
chk("plugins-empty", !navigator.plugins || navigator.plugins.length === 0,
`length=${navigator.plugins && navigator.plugins.length}`);
chk("permissions-notifications-quirk", false, "checked async below");
(async () => {
try {
const p = await navigator.permissions.query({ name: "notifications" });
// Headless Chrome historically returned "denied" while Notification.permission === "default".
const mismatch = p.state === "denied" && Notification.permission === "default";
checks.push({ name: "permissions-notifications-quirk", bad: mismatch,
note: `perm=${p.state} api=${Notification.permission}` });
finalize();
} catch (e) {
checks.push({ name: "permissions-notifications-quirk", bad: false, note: "n/a " + e.message });
finalize();
}
})();
function finalize() {
const rep = document.getElementById("rep");
rep.textContent = checks.map(c => `${c.bad ? "✗" : "✓"} ${c.name} ${c.note}`).join("\n");
const failed = checks.filter(c => c.bad);
if (failed.length) Challenge.fail(...failed.map(c => `${c.name}: ${c.note}`));
else Challenge.pass("all runtime checks clean");
}
</script>
</body>
@@ -0,0 +1,34 @@
<!doctype html>
<meta charset="utf-8">
<title>04 mouse entropy</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: click the green button after moving the mouse in an organic path. Page requires
≥15 <code>mousemove</code> events with non-zero <code>movementX/Y</code> variance before the click.</p>
<button id="go" style="padding:20px 32px;font-size:18px;background:#1f7a1f;color:#fff;border:0;border-radius:6px">Click me</button>
</main>
<script>
Challenge.init({
id: "mouse-entropy",
instructions: "click the button after natural mouse movement",
});
const moves = [];
window.addEventListener("mousemove", (e) => {
moves.push({ x: e.clientX, y: e.clientY, mx: e.movementX, my: e.movementY, t: e.timeStamp });
if (moves.length > 200) moves.shift();
});
document.getElementById("go").addEventListener("click", (e) => {
if (moves.length < 15) return Challenge.fail(`only ${moves.length} mousemove events before click`);
const dx = moves.map(m => Math.abs(m.mx)).reduce((a,b)=>a+b,0);
const dy = moves.map(m => Math.abs(m.my)).reduce((a,b)=>a+b,0);
if (dx + dy < 50) return Challenge.fail(`movement deltas tiny: dx=${dx}, dy=${dy}`);
// Detect "teleport then click" — all moves clustered at the button.
const r = document.getElementById("go").getBoundingClientRect();
const inside = moves.filter(m => m.x>=r.left && m.x<=r.right && m.y>=r.top && m.y<=r.bottom).length;
if (inside === moves.length) return Challenge.fail("all mousemoves inside target rect (teleport)");
Challenge.pass(`${moves.length} moves, |dx|+|dy|=${dx+dy}, ${inside} inside target`);
});
</script>
</body>
@@ -0,0 +1,34 @@
<!doctype html>
<meta charset="utf-8">
<title>05 event timing</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: click the button 3 times. Page rejects when <code>pointerdown→pointerup</code> is &lt;30ms
or always identical, and when between-click gaps are too uniform.</p>
<button id="go" style="padding:20px 32px;font-size:18px;background:#1f7a1f;color:#fff;border:0;border-radius:6px">Click me (0/3)</button>
</main>
<script>
Challenge.init({ id: "event-timing", instructions: "click button 3 times with human-like timing" });
const btn = document.getElementById("go");
const ups = [], downs = [], clickTs = [];
btn.addEventListener("pointerdown", (e) => downs.push(e.timeStamp));
btn.addEventListener("pointerup", (e) => ups.push(e.timeStamp));
btn.addEventListener("click", (e) => {
clickTs.push(e.timeStamp);
btn.textContent = `Click me (${clickTs.length}/3)`;
if (clickTs.length < 3) return;
const holds = downs.map((d,i)=> (ups[i]??d) - d);
const allSame = holds.every(h => Math.abs(h - holds[0]) < 0.5);
const tooFast = holds.some(h => h < 30);
const gaps = clickTs.slice(1).map((t,i)=> t - clickTs[i]);
const gapsUniform = gaps.length > 1 && Math.abs(gaps[0]-gaps[1]) < 5;
Challenge.log("timing", { holds, gaps });
if (tooFast) return Challenge.fail(`pointer hold too short: ${holds.join(",")}ms`);
if (allSame) return Challenge.fail(`pointer holds identical: ${holds.join(",")}ms`);
if (gapsUniform) return Challenge.fail(`between-click gaps suspiciously uniform: ${gaps.join(",")}ms`);
Challenge.pass(`holds=${holds.map(h=>h.toFixed(1)).join(",")}ms gaps=${gaps.map(g=>g.toFixed(0)).join(",")}ms`);
});
</script>
</body>
@@ -0,0 +1,29 @@
<!doctype html>
<meta charset="utf-8">
<title>06 click coordinates</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: click the green button 5 times. Page rejects when clicks always land on the exact element center.</p>
<button id="go" style="padding:24px 40px;font-size:18px;background:#1f7a1f;color:#fff;border:0;border-radius:6px">Click me (0/5)</button>
</main>
<script>
Challenge.init({ id: "click-coordinates", instructions: "click button 5 times; coordinates must vary" });
const btn = document.getElementById("go");
const pts = [];
btn.addEventListener("click", (e) => {
const r = btn.getBoundingClientRect();
const cx = r.left + r.width/2, cy = r.top + r.height/2;
pts.push({ x: e.clientX, y: e.clientY, dx: e.clientX-cx, dy: e.clientY-cy });
btn.textContent = `Click me (${pts.length}/5)`;
if (pts.length < 5) return;
const onCenter = pts.filter(p => Math.abs(p.dx) < 0.51 && Math.abs(p.dy) < 0.51).length;
const unique = new Set(pts.map(p => `${p.x},${p.y}`)).size;
Challenge.log("coords", { pts });
if (onCenter >= 4) return Challenge.fail(`${onCenter}/5 clicks on exact center`);
if (unique <= 1) return Challenge.fail(`only ${unique} unique click coords`);
Challenge.pass(`${unique} unique coords, ${onCenter} on center`);
});
</script>
</body>
@@ -0,0 +1,29 @@
<!doctype html>
<meta charset="utf-8">
<title>07 pointer properties</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: click the button. Page inspects <code>pointerType</code>, <code>pressure</code>,
<code>movementX/Y</code> on the preceding pointermove, and that pointerId is non-zero.</p>
<button id="go" style="padding:20px 32px;font-size:18px;background:#1f7a1f;color:#fff;border:0;border-radius:6px">Click me</button>
</main>
<script>
Challenge.init({ id: "pointer-properties", instructions: "click button; pointer event details must look real" });
const btn = document.getElementById("go");
let lastMove = null;
window.addEventListener("pointermove", (e) => { lastMove = { mx: e.movementX, my: e.movementY, pid: e.pointerId, type: e.pointerType }; });
btn.addEventListener("pointerdown", (e) => {
Challenge.log("pointerdown", { type: e.pointerType, pressure: e.pressure, pid: e.pointerId, mx: e.movementX, my: e.movementY });
const bad = [];
if (e.pointerType !== "mouse" && e.pointerType !== "touch" && e.pointerType !== "pen") bad.push(`pointerType=${e.pointerType}`);
if (e.pointerType === "mouse" && e.pressure !== 0.5) bad.push(`mouse pressure=${e.pressure} (real=0.5)`);
if (e.pointerId === 0) bad.push("pointerId=0");
if (!lastMove) bad.push("no preceding pointermove");
else if (lastMove.mx === 0 && lastMove.my === 0) bad.push("preceding pointermove had movementX=movementY=0");
if (bad.length) Challenge.fail(...bad);
else Challenge.pass(`type=${e.pointerType} pressure=${e.pressure} pid=${e.pointerId}`);
});
</script>
</body>
@@ -0,0 +1,37 @@
<!doctype html>
<meta charset="utf-8">
<title>08 keyboard cadence</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: type <code>secret</code>. Page demands per-key keydown+keypress+keyup with non-uniform gaps and non-zero hold time.</p>
<input id="t" placeholder="type 'secret'" style="font-size:18px;padding:8px 12px;width:240px">
</main>
<script>
Challenge.init({ id: "keyboard-cadence", instructions: "type 'secret' with realistic per-key cadence" });
const t = document.getElementById("t");
const evs = [];
["keydown","keypress","keyup","input"].forEach(name => {
t.addEventListener(name, (e) => evs.push({ name, key: e.key ?? e.data, t: e.timeStamp, trusted: e.isTrusted }));
});
t.addEventListener("keyup", () => {
if (t.value !== "secret") return;
const downs = evs.filter(e => e.name === "keydown");
const presses = evs.filter(e => e.name === "keypress");
const ups = evs.filter(e => e.name === "keyup");
const bad = [];
if (downs.length < 6) bad.push(`only ${downs.length} keydowns (need >=6)`);
if (presses.length < 6) bad.push(`only ${presses.length} keypress events`);
if (ups.length < 6) bad.push(`only ${ups.length} keyups`);
const holds = downs.slice(0, ups.length).map((d,i)=> ups[i].t - d.t);
if (holds.some(h => h <= 0)) bad.push(`some keyup at-or-before keydown: ${holds.join(",")}`);
if (holds.length && holds.every(h => Math.abs(h-holds[0]) < 0.5)) bad.push(`hold times identical: ${holds.join(",")}`);
const gaps = downs.slice(1).map((d,i)=> d.t - downs[i].t);
if (gaps.length && gaps.every(g => Math.abs(g-gaps[0]) < 0.5)) bad.push(`keydown gaps identical: ${gaps.join(",")}`);
Challenge.log("cadence", { holds, gaps });
if (bad.length) Challenge.fail(...bad);
else Challenge.pass(`holds=${holds.map(h=>h.toFixed(0)).join(",")} gaps=${gaps.map(g=>g.toFixed(0)).join(",")}`);
});
</script>
</body>
@@ -0,0 +1,45 @@
<!doctype html>
<meta charset="utf-8">
<title>09 framework input invariants</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: type <code>abc</code>. Page asserts React-style invariants: <code>beforeinput</code>
fires <em>before</em> <code>input</code>, both per-character, <code>inputType==="insertText"</code>,
value mutates between events, and no <code>compositionstart</code> for plain typing.</p>
<input id="t" placeholder="type abc" style="font-size:18px;padding:8px 12px;width:240px">
</main>
<script>
Challenge.init({ id: "composition-input", instructions: "type 'abc' producing framework-correct event order" });
const t = document.getElementById("t");
const evs = [];
let sawCompositionStart = false;
["beforeinput","input","compositionstart","compositionend"].forEach(name => {
t.addEventListener(name, (e) => {
evs.push({ name, t: e.timeStamp, inputType: e.inputType, data: e.data, value: t.value });
if (name === "compositionstart") sawCompositionStart = true;
});
});
t.addEventListener("input", () => {
if (t.value !== "abc") return;
const before = evs.filter(e => e.name === "beforeinput");
const inp = evs.filter(e => e.name === "input");
const bad = [];
if (sawCompositionStart) bad.push("compositionstart fired for plain ASCII typing");
if (before.length !== 3) bad.push(`beforeinput count=${before.length} (need 3)`);
if (inp.length !== 3) bad.push(`input count=${inp.length} (need 3)`);
if (before.some(b => b.inputType !== "insertText")) bad.push(`beforeinput.inputType=${before.map(b=>b.inputType).join(",")}`);
// beforeinput must precede matching input.
for (let i = 0; i < Math.min(before.length, inp.length); i++) {
if (before[i].t > inp[i].t) bad.push(`beforeinput[${i}] after input[${i}]`);
}
// Value must reflect each char incrementally.
const seq = inp.map(e => e.value).join("|");
if (seq !== "a|ab|abc") bad.push(`value seq at input events: ${seq} (need a|ab|abc)`);
Challenge.log("seq", { evs });
if (bad.length) Challenge.fail(...bad);
else Challenge.pass(`event order and invariants OK; seq=${seq}`);
});
</script>
</body>
@@ -0,0 +1,40 @@
<!doctype html>
<meta charset="utf-8">
<title>10 user activation gates</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: click the button. Handler then tries gated APIs (clipboard.writeText, fullscreen).
Success requires <code>navigator.userActivation.isActive</code> and at least one gated API to succeed.</p>
<button id="go" style="padding:20px 32px;font-size:18px;background:#1f7a1f;color:#fff;border:0;border-radius:6px">Click me</button>
</main>
<script>
Challenge.init({ id: "user-activation", instructions: "click button; user-activation must unlock gated APIs" });
document.getElementById("go").addEventListener("click", async () => {
// Capture activation state synchronously before any await (activation can be consumed by gated APIs).
const ua = navigator.userActivation;
const wasActive = !!ua?.isActive;
const hadBeenActive = !!ua?.hasBeenActive;
Challenge.log("activation", { isActive: wasActive, hasBeenActive: hadBeenActive });
let clip = "skip", fs = "skip";
try { await navigator.clipboard.writeText("pi-chrome-test"); clip = "ok"; }
catch (e) { clip = "err:" + e.name; }
try { await document.documentElement.requestFullscreen(); fs = "ok"; document.exitFullscreen?.(); }
catch (e) { fs = "err:" + e.name; }
Challenge.log("gates", { clip, fs });
if (!wasActive && !hadBeenActive) {
return Challenge.fail("userActivation.isActive/hasBeenActive both false (synthetic click)");
}
const okCount = [clip, fs].filter(x => x === "ok").length;
if (okCount === 0) {
return Challenge.skip(`activation present, but gated APIs blocked by environment/policy: clipboard=${clip}; fullscreen=${fs}`);
}
const warnings = [];
if (clip.startsWith("err")) warnings.push("clipboard.writeText " + clip);
if (fs.startsWith("err")) warnings.push("requestFullscreen " + fs);
if (warnings.length) Challenge.pass(`activation active; ${okCount}/2 gates succeeded; ${warnings.join("; ")}`);
else Challenge.pass(`activation active; clipboard=${clip}; fullscreen=${fs}`);
});
</script>
</body>
@@ -0,0 +1,36 @@
<!doctype html>
<meta charset="utf-8">
<title>11 honeypot fields</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: fill <em>only</em> the visible "Name" field with <code>alex</code> and submit.
Three honeypots (a, b, c) are present that a human cannot see/tab to. Filling any honeypot fails.</p>
<form id="f">
<label>Name <input name="name" id="name"></label>
<!-- off-screen -->
<input name="a" aria-hidden="true" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px;top:-9999px">
<!-- display:none -->
<input name="b" aria-hidden="true" tabindex="-1" autocomplete="off" style="display:none">
<!-- zero-size + visually hidden -->
<input name="c" aria-hidden="true" tabindex="-1" autocomplete="off"
style="opacity:0;width:0;height:0;border:0;padding:0">
<button type="submit">Submit</button>
</form>
</main>
<script>
Challenge.init({ id: "honeypot", instructions: "fill only the Name field with 'alex', then submit" });
const form = document.getElementById("f");
form.addEventListener("submit", (e) => {
e.preventDefault();
const fd = new FormData(form);
const filled = ["a","b","c"].filter(k => (fd.get(k) ?? "").toString().length > 0);
Challenge.log("submit", { name: fd.get("name"), a: fd.get("a"), b: fd.get("b"), c: fd.get("c") });
if (filled.length) return Challenge.fail("honeypot(s) filled: " + filled.join(","));
if ((fd.get("name") ?? "") !== "alex") return Challenge.fail(`name="${fd.get("name")}" (need 'alex')`);
Challenge.pass("only visible field filled");
});
</script>
</body>
@@ -0,0 +1,59 @@
<!doctype html>
<meta charset="utf-8">
<title>12 fingerprint consistency</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Auto-check. Looks for cross-API consistency mismatches that betray instrumented Chrome.</p>
<pre id="rep" style="font:12px monospace;background:#111;color:#eee;padding:12px;border-radius:6px"></pre>
</main>
<script>
Challenge.init({ id: "fingerprint", instructions: "no interaction" });
(async () => {
const checks = [];
const ua = navigator.userAgent;
const isChrome = /Chrome\/\d+/.test(ua);
checks.push({ name: "ua-is-chrome", bad: !isChrome, note: ua });
// UA-CH: Chrome should expose userAgentData with brands incl Chromium.
const uad = navigator.userAgentData;
const hasUAD = !!uad;
checks.push({ name: "userAgentData-present", bad: isChrome && !hasUAD, note: hasUAD ? JSON.stringify(uad.brands) : "missing" });
// Chrome runtime object exists in normal Chrome.
checks.push({ name: "window.chrome", bad: isChrome && typeof window.chrome === "undefined",
note: typeof window.chrome });
// languages must match accept-language semantics.
checks.push({ name: "languages-includes-language", bad: !navigator.languages?.includes(navigator.language),
note: `${navigator.language} vs ${JSON.stringify(navigator.languages)}` });
// WebGL vendor/renderer should not be Brian Paul / SwiftShader on user profile.
try {
const gl = document.createElement("canvas").getContext("webgl");
const dbg = gl.getExtension("WEBGL_debug_renderer_info");
const vendor = dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR);
const renderer = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
const swiftshader = /SwiftShader|llvmpipe|Software/i.test(renderer);
checks.push({ name: "webgl-not-software", bad: false, warn: swiftshader, note: `${vendor} / ${renderer}${swiftshader ? " (software renderer; warning in VM/remote contexts)" : ""}` });
} catch (e) {
checks.push({ name: "webgl-not-software", bad: false, note: "n/a " + e.message });
}
// hardwareConcurrency / deviceMemory plausibility.
checks.push({ name: "hardwareConcurrency", bad: !(navigator.hardwareConcurrency > 0), note: String(navigator.hardwareConcurrency) });
// navigator.webdriver again, separate from challenge 03 so it appears here too.
checks.push({ name: "webdriver-false", bad: navigator.webdriver === true, note: String(navigator.webdriver) });
const rep = document.getElementById("rep");
rep.textContent = checks.map(c => `${c.bad ? "✗" : c.warn ? "!" : "✓"} ${c.name} ${c.note}`).join("\n");
const failed = checks.filter(c => c.bad);
const warned = checks.filter(c => c.warn);
if (failed.length) Challenge.fail(...failed.map(c => `${c.name}: ${c.note}`));
else if (warned.length) Challenge.warn(...warned.map(c => `${c.name}: ${c.note}`));
else Challenge.pass("fingerprint consistent with real Chrome");
})();
</script>
</body>
@@ -0,0 +1,31 @@
<!doctype html>
<meta charset="utf-8">
<title>13 focus order</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: focus the input by <em>clicking</em> it (not <code>.focus()</code>), then type <code>x</code>.
Pointerdown must precede focus, and <code>:focus-visible</code> must be false for pointer focus.</p>
<input id="t" placeholder="click then type x" style="font-size:18px;padding:8px 12px;width:240px">
</main>
<script>
Challenge.init({ id: "focus-order", instructions: "click input then type 'x'" });
const t = document.getElementById("t");
let lastPointer = -Infinity, lastFocus = -Infinity, sawPointer = false;
t.addEventListener("pointerdown", (e) => { lastPointer = e.timeStamp; sawPointer = true; });
t.addEventListener("focus", (e) => {
lastFocus = e.timeStamp;
if (!sawPointer) Challenge.fail("focus arrived with no preceding pointerdown");
else if (lastFocus < lastPointer) Challenge.fail("focus before pointerdown");
});
t.addEventListener("input", () => {
if (t.value !== "x") return;
// For pointer-driven focus, :focus-visible should be false.
const fv = t.matches(":focus-visible");
Challenge.log("focus-visible", { fv });
if (fv) Challenge.fail(":focus-visible true after pointer click (looks like keyboard focus)");
else if (window.__verdict !== "FAIL") Challenge.pass("pointerdown→focus→input order ok, :focus-visible=false");
});
</script>
</body>
@@ -0,0 +1,28 @@
<!doctype html>
<meta charset="utf-8">
<title>14 wheel scroll</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: scroll the box to the bottom. Page rejects raw <code>scrollTop</code> assignment without
<code>wheel</code> events.</p>
<div id="box" style="height:200px;overflow:auto;border:1px solid #555;background:#fff;color:#111;padding:8px">
<div style="height:1200px">scroll me ⬇<br><br>...lots of content...</div>
</div>
</main>
<script>
Challenge.init({ id: "wheel-scroll", instructions: "scroll the box to its bottom" });
const box = document.getElementById("box");
let wheelCount = 0, lastWheelTs = -Infinity;
box.addEventListener("wheel", (e) => { wheelCount++; lastWheelTs = e.timeStamp; Challenge.log("wheel", { dy: e.deltaY }); }, { passive: true });
box.addEventListener("scroll", () => {
Challenge.log("scroll", { top: box.scrollTop });
if (box.scrollTop + box.clientHeight >= box.scrollHeight - 2) {
if (wheelCount === 0) Challenge.fail("scrolled to bottom with zero wheel events");
else if (performance.now() - lastWheelTs > 1500) Challenge.fail("wheel events too far before final scroll");
else Challenge.pass(`${wheelCount} wheel events accompanied scroll`);
}
});
</script>
</body>
@@ -0,0 +1,73 @@
<!doctype html>
<meta charset="utf-8">
<title>15 drag-drop DataTransfer</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: drag the <b>red box</b> into the <b>drop zone</b>. Page asserts a full HTML5
drag sequence with a populated <code>DataTransfer</code> — synthetic pointer drags
dispatched without <code>dragstart</code>/<code>drop</code> + DataTransfer payload fail.</p>
<div style="display:flex;gap:24px;align-items:center;margin-top:16px">
<div id="src" draggable="true"
style="width:80px;height:80px;background:#c33;color:#fff;display:flex;align-items:center;justify-content:center;border-radius:6px;cursor:grab">
DRAG
</div>
<div id="dst"
style="width:200px;height:120px;border:2px dashed #888;display:flex;align-items:center;justify-content:center;color:#888">
DROP HERE
</div>
</div>
</main>
<script>
Challenge.init({ id: "drag-drop-datatransfer", instructions: "drag red box into drop zone via real HTML5 drag" });
const src = document.getElementById("src");
const dst = document.getElementById("dst");
const seen = { dragstart: 0, drag: 0, dragenter: 0, dragover: 0, drop: 0, dragend: 0 };
let dtHadTypes = false, dtPayload = null, isTrustedAll = true;
src.addEventListener("dragstart", (e) => {
seen.dragstart++;
if (!e.isTrusted) isTrustedAll = false;
try { e.dataTransfer.setData("text/plain", "payload-" + Math.random().toString(36).slice(2,8)); } catch {}
Challenge.log("dragstart", { hasDt: !!e.dataTransfer });
});
src.addEventListener("drag", (e) => { seen.drag++; if (!e.isTrusted) isTrustedAll = false; });
src.addEventListener("dragend", (e) => { seen.dragend++; if (!e.isTrusted) isTrustedAll = false; });
dst.addEventListener("dragenter", (e) => { seen.dragenter++; e.preventDefault(); if (!e.isTrusted) isTrustedAll = false; });
dst.addEventListener("dragover", (e) => { seen.dragover++; e.preventDefault(); if (!e.isTrusted) isTrustedAll = false; });
dst.addEventListener("drop", (e) => {
e.preventDefault();
seen.drop++;
if (!e.isTrusted) isTrustedAll = false;
const dt = e.dataTransfer;
if (dt) {
dtHadTypes = (dt.types && dt.types.length > 0);
try { dtPayload = dt.getData("text/plain"); } catch {}
}
Challenge.log("drop", { types: dt && [...dt.types], payload: dtPayload });
// dragend fires AFTER drop per spec; give it a tick.
setTimeout(evaluate, 50);
});
function evaluate() {
const bad = [];
if (!isTrustedAll) bad.push("at least one drag event isTrusted=false");
for (const k of ["dragstart","dragover","drop","dragend"]) {
if (!seen[k]) bad.push(`missing ${k}`);
}
if (!dtHadTypes) bad.push("DataTransfer.types empty on drop");
if (!dtPayload || !dtPayload.startsWith("payload-")) bad.push("DataTransfer payload missing on drop");
if (bad.length) Challenge.fail(...bad);
else Challenge.pass(`full drag cycle, payload=${dtPayload}`);
}
// Fallback: pointer-drag without dragstart triggers fail after a delay.
let pointerDownInSrc = false;
src.addEventListener("pointerdown", () => { pointerDownInSrc = true;
setTimeout(() => { if (!seen.dragstart && pointerDownInSrc) Challenge.fail("pointerdown on src but no dragstart fired"); }, 1500);
});
</script>
</body>
@@ -0,0 +1,54 @@
<!doctype html>
<meta charset="utf-8">
<title>16 contenteditable selection</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: focus the editable region and type <code>hello</code>. Page asserts that
<code>window.getSelection()</code> reflects per-keystroke caret movement
(<code>rangeCount===1</code>, <code>collapsed</code> caret, anchor inside the editor,
offset advances 1 per char) and that <code>selectionchange</code> fires.</p>
<div id="ed" contenteditable="true"
style="min-height:60px;padding:10px;border:1px solid #555;background:#fff;color:#111;font:16px monospace"></div>
</main>
<script>
Challenge.init({ id: "contenteditable-selection", instructions: "focus editor and type 'hello'" });
const ed = document.getElementById("ed");
const offsets = [];
let selChanges = 0;
document.addEventListener("selectionchange", () => {
selChanges++;
const s = window.getSelection();
if (!s || s.rangeCount === 0) return;
if (!ed.contains(s.anchorNode)) return;
offsets.push({ off: s.anchorOffset, collapsed: s.isCollapsed, t: performance.now(), len: ed.textContent.length });
});
ed.addEventListener("input", async () => {
if (ed.textContent !== "hello") return;
// selectionchange is async — wait a tick so the final caret update is observed.
await new Promise((r) => setTimeout(r, 30));
const bad = [];
const sel = window.getSelection();
if (!sel) bad.push("getSelection()==null");
else {
if (sel.rangeCount !== 1) bad.push(`rangeCount=${sel.rangeCount} (need 1)`);
if (!sel.isCollapsed) bad.push("selection not collapsed after typing");
if (!ed.contains(sel.anchorNode)) bad.push("selection anchor outside editor");
if (sel.anchorOffset !== 5) bad.push(`anchorOffset=${sel.anchorOffset} (need 5)`);
}
if (selChanges < 5) bad.push(`selectionchange count=${selChanges} (need ≥5)`);
// offsets should advance monotonically 1,2,3,4,5 (allowing extras from focus)
const monotonic = offsets.map(o => o.off);
const peak = Math.max(0, ...monotonic);
if (peak !== 5) bad.push(`peak caret offset=${peak} during typing (need 5)`);
// every caret reading must be collapsed (no spurious ranges)
if (offsets.some(o => !o.collapsed)) bad.push("non-collapsed range observed during typing");
Challenge.log("sel", { offsets, selChanges });
if (bad.length) Challenge.fail(...bad);
else Challenge.pass(`selectionchanges=${selChanges}, caret advanced 0→5`);
});
</script>
</body>
@@ -0,0 +1,48 @@
<!doctype html>
<meta charset="utf-8">
<title>17 paste clipboard</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: paste the string <code>pi-chrome</code> into the input via a real OS paste
(Cmd/Ctrl+V firing a trusted <code>paste</code> event with populated
<code>clipboardData</code>). Programmatic <code>value=</code> or <code>setRangeText</code>
fail. The <code>input</code> event's <code>inputType</code> must be
<code>insertFromPaste</code>.</p>
<input id="t" placeholder="paste here" style="font-size:18px;padding:8px 12px;width:280px">
</main>
<script>
Challenge.init({ id: "paste-clipboard", instructions: "paste 'pi-chrome' via OS clipboard" });
const t = document.getElementById("t");
let sawPaste = false, pasteIsTrusted = false, pastePayload = null;
t.addEventListener("paste", (e) => {
sawPaste = true;
pasteIsTrusted = e.isTrusted;
try { pastePayload = e.clipboardData && e.clipboardData.getData("text/plain"); } catch {}
Challenge.log("paste", { isTrusted: e.isTrusted, payload: pastePayload });
});
let lastInputType = null;
t.addEventListener("input", (e) => {
lastInputType = e.inputType;
if (t.value !== "pi-chrome") return;
const bad = [];
if (!sawPaste) bad.push("no paste event fired before value matched");
if (!pasteIsTrusted) bad.push("paste event isTrusted=false");
if (pastePayload !== "pi-chrome") bad.push(`clipboardData payload="${pastePayload}" (need 'pi-chrome')`);
if (lastInputType !== "insertFromPaste") bad.push(`input.inputType="${lastInputType}" (need 'insertFromPaste')`);
if (bad.length) Challenge.fail(...bad);
else Challenge.pass("trusted paste with clipboardData text/plain match");
});
// Also fail loudly if value is set without any input event (raw .value=)
let inputFired = false;
t.addEventListener("input", () => { inputFired = true; });
const obs = new MutationObserver(() => {});
obs.observe(t, { attributes: true, attributeFilter: ["value"] });
setInterval(() => {
if (t.value === "pi-chrome" && !inputFired) Challenge.fail("value matched without input event firing");
}, 200);
</script>
</body>
@@ -0,0 +1,56 @@
<!doctype html>
<meta charset="utf-8">
<title>18 native select option</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: select the option <code>banana</code> from the native <code>&lt;select&gt;</code>.
Page asserts a real picker interaction: <code>mousedown</code>/<code>pointerdown</code>
on the &lt;select&gt; (opening the OS picker), then a <code>change</code> event with
<code>isTrusted=true</code>. Programmatically setting <code>.value</code> or clicking the
option element directly fails (clicking &lt;option&gt; does not change selection in
real browsers).</p>
<select id="s" style="font-size:18px;padding:6px 10px">
<option value="">— pick a fruit —</option>
<option value="apple">apple</option>
<option value="banana">banana</option>
<option value="cherry">cherry</option>
</select>
</main>
<script>
Challenge.init({ id: "native-select", instructions: "pick 'banana' from native select" });
const s = document.getElementById("s");
let sawPointerDownOnSelect = false, pointerDownTrusted = false;
s.addEventListener("pointerdown", (e) => { sawPointerDownOnSelect = true; pointerDownTrusted = e.isTrusted; });
s.addEventListener("mousedown", (e) => { sawPointerDownOnSelect = true; pointerDownTrusted ||= e.isTrusted; });
// Trap: if a click bubbles up from an <option>, that's bot-like (real OS pickers don't dispatch this).
s.addEventListener("click", (e) => {
if (e.target && e.target.tagName === "OPTION") {
Challenge.fail("click event with target=<option> bubbled to <select> (synthetic option click)");
}
});
let changeTrusted = false;
s.addEventListener("change", (e) => {
changeTrusted = e.isTrusted;
Challenge.log("change", { v: s.value, isTrusted: e.isTrusted });
if (s.value !== "banana") return Challenge.fail(`selected="${s.value}" (need 'banana')`);
const bad = [];
if (!changeTrusted) bad.push("change event isTrusted=false");
if (!sawPointerDownOnSelect) bad.push("no pointerdown/mousedown on <select> before change");
if (!pointerDownTrusted) bad.push("pointerdown on select isTrusted=false");
if (bad.length) Challenge.fail(...bad);
else Challenge.pass("native picker opened and trusted change fired");
});
// Also trip if value mutates without change firing.
let changed = false;
s.addEventListener("change", () => { changed = true; });
setInterval(() => {
if (s.value === "banana" && !changed) Challenge.fail("value='banana' without any change event");
}, 200);
</script>
</body>
@@ -0,0 +1,50 @@
<!doctype html>
<meta charset="utf-8">
<title>19 hover dwell</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: hover the trigger and wait until the <b>Confirm</b> button reveals (≥600ms dwell
with continuous <code>pointermove</code> activity inside the trigger), then click Confirm.
Instant hover-then-click without dwell, or hover with no intermediate pointermove,
looks robotic and fails.</p>
<div id="trig" style="display:inline-block;padding:16px 24px;background:#2a4;color:#fff;border-radius:6px;cursor:pointer">
hover me
</div>
<button id="go" style="display:none;margin-left:12px;padding:10px 18px;background:#1f7a1f;color:#fff;border:0;border-radius:6px">Confirm</button>
</main>
<script>
Challenge.init({ id: "hover-dwell", instructions: "hover ≥600ms with movement, then click Confirm" });
const trig = document.getElementById("trig");
const btn = document.getElementById("go");
let enterTs = 0, lastMoveTs = 0, moveCount = 0;
trig.addEventListener("pointerenter", (e) => { enterTs = e.timeStamp; lastMoveTs = e.timeStamp; moveCount = 0; });
trig.addEventListener("pointermove", (e) => { lastMoveTs = e.timeStamp; moveCount++; });
trig.addEventListener("pointerleave", () => {
if (!btn.dataset.revealed) { enterTs = 0; lastMoveTs = 0; moveCount = 0; }
});
// Reveal after sustained dwell + activity.
setInterval(() => {
if (btn.dataset.revealed) return;
if (!enterTs) return;
const dwell = performance.now() - enterTs;
if (dwell >= 600 && moveCount >= 3) {
btn.style.display = "inline-block";
btn.dataset.revealed = "1";
btn.dataset.revealTs = performance.now();
Challenge.log("revealed", { dwell, moveCount });
}
}, 50);
btn.addEventListener("click", (e) => {
if (!btn.dataset.revealed) return Challenge.fail("Confirm clicked before reveal (display:none)");
const sinceReveal = e.timeStamp - Number(btn.dataset.revealTs);
if (sinceReveal < 80) return Challenge.fail(`clicked ${sinceReveal.toFixed(0)}ms after reveal (too instant)`);
if (!e.isTrusted) return Challenge.fail("Confirm click isTrusted=false");
Challenge.pass(`dwell+activity+human-latency reveal (Δ=${sinceReveal.toFixed(0)}ms)`);
});
</script>
</body>
@@ -0,0 +1,78 @@
<!doctype html>
<meta charset="utf-8">
<title>20 React _valueTracker</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: set the input to <code>react-ok</code> in a way that React would accept.
The page mimics React's <code>_valueTracker</code>: it stamps the input with a
hidden tracker holding the last-seen value. An <code>input</code> event is only
considered "framework-real" if the tracker's recorded value differs from the
current <code>input.value</code> at the moment the event fires
(i.e. somebody used the native <code>HTMLInputElement.value</code> setter and
dispatched <code>input</code> — what React internals require).</p>
<p>Setting <code>el.value = ...</code> directly and dispatching a synthetic <code>input</code>
fails because the native setter wasn't used. Calling <code>setRangeText</code> without
dispatching <code>input</code> also fails.</p>
<input id="t" placeholder="type or fill 'react-ok'" style="font-size:18px;padding:8px 12px;width:240px">
</main>
<script>
Challenge.init({ id: "react-value-tracker", instructions: "make input value 'react-ok' as React expects" });
const t = document.getElementById("t");
// Install a fake React-style _valueTracker.
(function attachTracker(node) {
const nativeProto = Object.getPrototypeOf(node);
const desc = Object.getOwnPropertyDescriptor(nativeProto, "value");
const nativeGet = desc.get, nativeSet = desc.set;
let tracked = nativeGet.call(node); // last value the "framework" has seen
node._valueTracker = {
getValue() { return tracked; },
setValue(v) { tracked = v; },
};
// Wrap the value property on the *instance* to intercept assignments.
Object.defineProperty(node, "value", {
configurable: true,
get() { return nativeGet.call(this); },
set(v) {
// If somebody assigns via instance property (the bad path), sync the tracker
// so the input event below will look "stale" and we can detect it.
tracked = String(v);
nativeSet.call(this, v);
},
});
})(t);
let passed = false;
t.addEventListener("input", (e) => {
if (passed) return;
const proto = Object.getPrototypeOf(t);
const nativeGet = Object.getOwnPropertyDescriptor(proto, "value").get;
const real = nativeGet.call(t);
const tracked = t._valueTracker.getValue();
Challenge.log("input", { real, tracked, isTrusted: e.isTrusted });
if (real !== "react-ok") return;
if (!e.isTrusted && tracked === real) {
return Challenge.fail("synthetic input event but _valueTracker already up-to-date (instance value= setter used, not native)");
}
// The "good" path: native setter ran (tracker still stale), then input dispatched.
// After the framework consumes the event it would syncs the tracker.
t._valueTracker.setValue(real);
passed = true;
Challenge.pass(`value 'react-ok' delivered with stale tracker (native setter path); isTrusted=${e.isTrusted}`);
});
// Trap: value mutates without input ever firing.
setInterval(() => {
if (passed) return;
const proto = Object.getPrototypeOf(t);
const nativeGet = Object.getOwnPropertyDescriptor(proto, "value").get;
if (nativeGet.call(t) === "react-ok" && !passed) {
// Only fail if we've been sitting on the value with no event for a while.
setTimeout(() => { if (!passed && nativeGet.call(t) === "react-ok") Challenge.fail("value='react-ok' but no input event consumed it"); }, 600);
}
}, 300);
</script>
</body>
@@ -0,0 +1,65 @@
<!doctype html>
<meta charset="utf-8">
<title>21 keyboard modifiers</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: focus the input and type a capital <code>A</code> via <b>Shift+a</b>
(not Caps Lock, not pasted, not assigned). Page asserts a real Shift chord:
<code>keydown</code> for <code>Shift</code> arrives <em>before</em> <code>keydown</code> for
<code>A</code>; the <code>A</code> event has <code>shiftKey=true</code>,
<code>key==="A"</code>, <code>code==="KeyA"</code>, <code>getModifierState("Shift")===true</code>;
and <code>keyup</code> for Shift arrives <em>after</em> <code>keyup</code> for <code>A</code>.</p>
<input id="t" placeholder="type Shift+a" style="font-size:18px;padding:8px 12px;width:240px">
</main>
<script>
Challenge.init({ id: "keyboard-modifiers", instructions: "type capital A using Shift+a chord" });
const t = document.getElementById("t");
const log = [];
for (const name of ["keydown","keypress","keyup","input"]) {
t.addEventListener(name, (e) => {
log.push({
name,
key: e.key, code: e.code,
shiftKey: e.shiftKey, isTrusted: e.isTrusted,
modShift: e.getModifierState ? e.getModifierState("Shift") : null,
t: e.timeStamp,
});
});
}
t.addEventListener("input", () => {
if (t.value !== "A") return;
const bad = [];
const downShift = log.find(e => e.name === "keydown" && e.key === "Shift");
const downA = log.find(e => e.name === "keydown" && e.code === "KeyA");
const upShift = log.find(e => e.name === "keyup" && e.key === "Shift");
const upA = log.find(e => e.name === "keyup" && e.code === "KeyA");
if (!downShift) bad.push("no keydown for Shift");
if (!downA) bad.push("no keydown for KeyA");
if (!upShift) bad.push("no keyup for Shift");
if (!upA) bad.push("no keyup for KeyA");
if (downShift && downA && downShift.t > downA.t) bad.push("Shift keydown after A keydown");
if (upShift && upA && upShift.t < upA.t) bad.push("Shift keyup before A keyup");
if (downA) {
if (downA.key !== "A") bad.push(`A keydown.key="${downA.key}" (need 'A')`);
if (downA.code !== "KeyA") bad.push(`A keydown.code="${downA.code}"`);
if (!downA.shiftKey) bad.push("A keydown.shiftKey=false");
if (downA.modShift !== true) bad.push("getModifierState('Shift')!==true on A keydown");
if (!downA.isTrusted) bad.push("A keydown isTrusted=false");
}
if (downShift) {
if (!downShift.isTrusted) bad.push("Shift keydown isTrusted=false");
}
Challenge.log("chord", { log });
if (bad.length) Challenge.fail(...bad);
else Challenge.pass("Shift+a chord, modifiers + ordering correct");
});
</script>
</body>
@@ -0,0 +1,66 @@
<!doctype html>
<meta charset="utf-8">
<title>22 touch events</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: tap the green pad on a touch-capable device (or via DevTools touch
emulation). Page asserts a real <code>touchstart</code>/<code>touchend</code> sequence:
<code>touches</code> / <code>changedTouches</code> are
<code>TouchList</code> objects containing <code>Touch</code> instances with
<code>identifier</code>, <code>clientX/Y</code>, <code>radiusX/Y</code>, and
<code>force</code> in [0,1]. Synthetic <code>PointerEvent</code> with
<code>pointerType="touch"</code> alone does not satisfy this — touch events must
fire too.</p>
<p><b>Note for caller:</b> requires Chrome touch emulation on (DevTools → Sensors →
"Touch: Force enabled") or a real touchscreen. This is intentional: pi-chrome's
synthetic <code>pointerType:'touch'</code> does NOT also dispatch TouchEvents.</p>
<div id="pad" style="width:200px;height:200px;background:#1f7a1f;border-radius:12px;color:#fff;display:flex;align-items:center;justify-content:center;font:18px monospace;touch-action:none;user-select:none">
TAP
</div>
</main>
<script>
Challenge.init({ id: "touch-events", instructions: "tap the pad on a touch-capable device" });
const pad = document.getElementById("pad");
let touchStart = null, touchEnd = null;
pad.addEventListener("touchstart", (e) => {
touchStart = e;
Challenge.log("touchstart", { tl: e.touches.length, ct: e.changedTouches.length, isTrusted: e.isTrusted });
}, { passive: true });
pad.addEventListener("touchend", (e) => {
touchEnd = e;
const bad = [];
if (!touchStart) return Challenge.fail("touchend without touchstart");
if (!e.isTrusted || !touchStart.isTrusted) bad.push("touch event isTrusted=false");
const ts = touchStart.changedTouches;
if (!ts || ts.length === 0) bad.push("touchstart.changedTouches empty");
else {
const T = ts[0];
// Window.Touch should exist as the constructor.
if (typeof window.Touch !== "function") bad.push("window.Touch constructor missing (no native TouchEvent support)");
if (!(T instanceof Touch)) bad.push("changedTouches[0] not instanceof Touch");
if (typeof T.identifier !== "number") bad.push("Touch.identifier not number");
if (!Number.isFinite(T.clientX) || !Number.isFinite(T.clientY)) bad.push("Touch.clientX/Y invalid");
if (!("force" in T) || typeof T.force !== "number" || T.force < 0 || T.force > 1) bad.push(`Touch.force=${T.force}`);
if (!("radiusX" in T) || typeof T.radiusX !== "number") bad.push("Touch.radiusX missing");
}
if (!(touchStart instanceof TouchEvent)) bad.push("touchstart not instanceof TouchEvent");
if (bad.length) Challenge.fail(...bad);
else Challenge.pass("real TouchEvent + Touch object surface present");
}, { passive: true });
// Capability check: if TouchEvent constructor missing entirely, hint immediately on click.
pad.addEventListener("click", () => {
if (!touchStart && typeof window.TouchEvent !== "function") {
Challenge.fail("TouchEvent constructor not available on this UA (enable touch emulation)");
} else if (!touchStart) {
Challenge.fail("click fired but no touchstart — synthetic pointer without TouchEvent");
}
});
</script>
</body>
@@ -0,0 +1,76 @@
<!doctype html>
<meta charset="utf-8">
<title>23 stack-trace fingerprint</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: click the button. The page samples call stacks from inside several
instrumented globals (<code>Function.prototype.toString</code>,
<code>document.querySelector</code>, <code>Element.prototype.click</code>) and inspects
the stack of <em>this script's own</em> handler. It fails if it sees telltales of
evaluator-injected frames (e.g. <code>at &lt;anonymous&gt;</code> as the only frame, the
bridge's <code>new Function</code> wrapper, <code>callFunctionOn</code>,
<code>executeScript</code>, or extension URLs).</p>
<button id="go" style="padding:14px 22px;font-size:16px;background:#1f7a1f;color:#fff;border:0;border-radius:6px">Click me</button>
</main>
<script>
Challenge.init({ id: "stack-trace-fingerprint", instructions: "click the button" });
// Suspicious stack-frame patterns commonly observed when code is invoked via
// CDP Runtime.evaluate / chrome.scripting.executeScript / new Function bodies
// dispatched from an MV3 service worker.
const SUSPICIOUS = [
/chrome-extension:\/\//i,
/\bnew Function\b/,
/Runtime\.evaluate/i,
/Runtime\.callFunctionOn/i,
/executeScript/i,
/content[_-]?script/i,
/^\s*at\s+eval\b/m,
];
function inspectStack(stack) {
if (!stack) return ["empty stack"];
const hits = SUSPICIOUS.filter(r => r.test(stack)).map(r => r.source);
const lines = stack.split("\n").filter(l => l.trim().startsWith("at "));
// Do not fail on generic <anonymous> frames alone: inline scripts, extensions,
// and browser versions vary here. This test should catch concrete automation
// tells, not punish legitimate stack formatting differences.
const reasons = [];
if (hits.length) reasons.push("suspicious frames: " + hits.join(","));
return reasons;
}
// Hook some commonly-touched APIs so any pre-click bridge instrumentation also
// leaves a trail. Their stacks get inspected the moment the click handler fires.
const probeStacks = [];
const oTo = Function.prototype.toString;
Function.prototype.toString = function () {
probeStacks.push({ where: "Function.toString", stack: new Error().stack });
return oTo.apply(this, arguments);
};
const oQS = Document.prototype.querySelector;
Document.prototype.querySelector = function (sel) {
probeStacks.push({ where: "document.querySelector", sel, stack: new Error().stack });
return oQS.apply(this, arguments);
};
document.getElementById("go").addEventListener("click", (e) => {
const ownStack = new Error().stack || "";
const reasons = inspectStack(ownStack);
// Also check any probe stacks gathered before the click — the bridge often
// queries the DOM right before dispatching.
const probeBad = [];
for (const p of probeStacks) {
const r = inspectStack(p.stack);
if (r.length) probeBad.push(`${p.where}: ${r.join("; ")}`);
}
Challenge.log("stacks", { ownStack, probeStacks });
if (!e.isTrusted) return Challenge.fail("click isTrusted=false");
if (reasons.length) return Challenge.fail(...reasons);
if (probeBad.length) return Challenge.fail(...probeBad.slice(0, 3));
Challenge.pass("call stack matches an in-page event handler");
});
</script>
</body>
@@ -0,0 +1,51 @@
<!doctype html>
<meta charset="utf-8">
<title>24 viewport edge clicks</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: click the button. Page rejects any pointer event whose
<code>clientX</code>/<code>clientY</code> falls outside the viewport
(<code>x &lt; 0</code>, <code>x &gt; innerWidth</code>, etc.) or lands on a coordinate
that's not actually inside the button's <code>getBoundingClientRect()</code>.
Real pointing devices can't dispatch clicks at negative coordinates;
bridges that pass uncoerced selector-center coords (or coordinates from a
stale layout) trip this.</p>
<button id="go" style="margin:40px;padding:14px 22px;font-size:16px;background:#1f7a1f;color:#fff;border:0;border-radius:6px">Click me</button>
</main>
<script>
Challenge.init({ id: "viewport-edge-clicks", instructions: "click the button at valid viewport coords" });
const btn = document.getElementById("go");
// Track ALL pointer/click events on document — even ones that miss the button.
let suspicious = null;
for (const name of ["pointerdown","pointerup","click","mousedown","mouseup"]) {
document.addEventListener(name, (e) => {
const x = e.clientX, y = e.clientY;
const w = window.innerWidth, h = window.innerHeight;
if (x < 0 || y < 0 || x > w || y > h) {
suspicious = `${name} at (${x},${y}) outside viewport ${w}x${h}`;
Challenge.fail(suspicious);
}
if ((x === 0 && y === 0) && e.isTrusted === false) {
suspicious = `${name} at (0,0) with isTrusted=false (default synthetic coords)`;
Challenge.fail(suspicious);
}
}, true);
}
btn.addEventListener("click", (e) => {
if (suspicious) return;
if (!e.isTrusted) return Challenge.fail("click isTrusted=false");
const r = btn.getBoundingClientRect();
const x = e.clientX, y = e.clientY;
const inside = x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
if (!inside) return Challenge.fail(`click at (${x},${y}) outside button rect ${JSON.stringify({l:r.left,t:r.top,r:r.right,b:r.bottom})}`);
// Coord must not be exactly integer-center (covered by #06 but reinforced here).
Challenge.log("click", { x, y, rect: r });
Challenge.pass(`click at (${x.toFixed(1)},${y.toFixed(1)}) inside button rect`);
});
</script>
</body>
@@ -0,0 +1,62 @@
<!doctype html>
<meta charset="utf-8">
<title>25 pointer continuity</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: click <b>A</b>, then click <b>B</b>. Page requires pointer continuity:
between the two clicks there must be intermediate <code>pointermove</code> events
whose path roughly connects the two click points (no teleport). Two clicks
&gt;100px apart with zero moves between is a synthetic-bridge tell.</p>
<div style="display:flex;justify-content:space-between;margin-top:20px">
<button id="a" style="padding:14px 22px;background:#36c;color:#fff;border:0;border-radius:6px">A</button>
<div style="flex:1"></div>
<button id="b" style="padding:14px 22px;background:#c36;color:#fff;border:0;border-radius:6px">B</button>
</div>
</main>
<script>
Challenge.init({ id: "pointer-continuity", instructions: "click A, move mouse to B, click B" });
const a = document.getElementById("a");
const b = document.getElementById("b");
const moves = [];
window.addEventListener("pointermove", (e) => {
moves.push({ x: e.clientX, y: e.clientY, t: e.timeStamp });
if (moves.length > 500) moves.shift();
});
let aClickTs = 0, aPoint = null;
a.addEventListener("click", (e) => {
if (!e.isTrusted) return Challenge.fail("A click isTrusted=false");
aClickTs = e.timeStamp;
aPoint = { x: e.clientX, y: e.clientY };
Challenge.log("A click", aPoint);
});
b.addEventListener("click", (e) => {
if (!aClickTs) return Challenge.fail("B clicked before A");
if (!e.isTrusted) return Challenge.fail("B click isTrusted=false");
const bPoint = { x: e.clientX, y: e.clientY };
const between = moves.filter(m => m.t > aClickTs && m.t < e.timeStamp);
Challenge.log("B click", { bPoint, betweenCount: between.length });
const dist = Math.hypot(bPoint.x - aPoint.x, bPoint.y - aPoint.y);
if (dist < 100) return Challenge.fail(`A-B distance only ${dist.toFixed(0)}px — buttons should be further apart in viewport`);
if (between.length < 5) return Challenge.fail(`only ${between.length} pointermove(s) between A and B (need ≥5)`);
// Path coverage: at least some intermediate move must be ≥30% of the way across.
const inSpan = between.filter(m => {
const px = (m.x - aPoint.x) / (bPoint.x - aPoint.x || 1);
return px > 0.25 && px < 0.75;
});
if (inSpan.length === 0) return Challenge.fail("no pointermove samples in mid-span between A and B (teleport)");
// Move timestamps should be spread, not bunched in <20ms.
const span = between[between.length-1].t - between[0].t;
if (span < 30) return Challenge.fail(`pointermoves bunched in ${span.toFixed(0)}ms (need spread ≥30ms)`);
Challenge.pass(`${between.length} moves bridging A→B over ${span.toFixed(0)}ms, dist=${dist.toFixed(0)}px`);
});
</script>
</body>
@@ -0,0 +1,57 @@
<!doctype html>
<meta charset="utf-8">
<title>26 mousemove rate</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: sweep the mouse across the box for ~1s, then click the green button.
Page measures the inter-event timestamp distribution of <code>mousemove</code>
events. Real pointing devices fire at ~60250 Hz (median Δt 418ms). Bridges
that flood synthetic moves on a tight <code>for</code>-loop or
<code>setTimeout(...,1)</code> produce a degenerate distribution
(most Δt &lt; 2ms, or all Δt identical, or median &gt; 50ms with no jitter).</p>
<div id="pad" style="height:160px;border:1px solid #555;background:#fff;color:#111;display:flex;align-items:center;justify-content:center;font:13px monospace;user-select:none">
sweep me
</div>
<button id="go" style="margin-top:14px;padding:12px 20px;background:#1f7a1f;color:#fff;border:0;border-radius:6px">Done</button>
</main>
<script>
Challenge.init({ id: "mousemove-rate", instructions: "sweep mouse across pad ~1s, then click Done" });
const pad = document.getElementById("pad");
const stamps = [];
pad.addEventListener("mousemove", (e) => { stamps.push(e.timeStamp); });
document.getElementById("go").addEventListener("click", (e) => {
if (!e.isTrusted) return Challenge.fail("Done click isTrusted=false");
if (stamps.length < 20) return Challenge.fail(`only ${stamps.length} mousemove events (need ≥20)`);
const deltas = [];
for (let i = 1; i < stamps.length; i++) deltas.push(stamps[i] - stamps[i-1]);
deltas.sort((a,b) => a-b);
const median = deltas[Math.floor(deltas.length/2)];
const p10 = deltas[Math.floor(deltas.length*0.1)];
const p90 = deltas[Math.floor(deltas.length*0.9)];
const mean = deltas.reduce((s,x)=>s+x,0) / deltas.length;
const variance = deltas.reduce((s,x)=>s+(x-mean)**2,0) / deltas.length;
const std = Math.sqrt(variance);
const allSame = deltas.every(d => Math.abs(d - deltas[0]) < 0.05);
const allTiny = deltas.every(d => d < 1.5);
Challenge.log("dist", { n: deltas.length, median, p10, p90, mean, std });
const bad = [];
if (allSame) bad.push(`all Δt identical (~${deltas[0].toFixed(2)}ms) — scripted loop`);
if (allTiny) bad.push(`all Δt < 1.5ms — synthetic flood`);
if (median < 2) bad.push(`median Δt ${median.toFixed(2)}ms too fast for a real device`);
if (median > 60) bad.push(`median Δt ${median.toFixed(0)}ms too slow (real moves ~418ms)`);
if (std < 0.5) bad.push(`Δt std=${std.toFixed(2)}ms — no jitter (scripted)`);
// Ratio p90/p10 should be ≥1.8 for organic motion.
if (p10 > 0 && (p90 / p10) < 1.5) bad.push(`p90/p10 ratio ${(p90/p10).toFixed(2)} — distribution too narrow`);
if (bad.length) Challenge.fail(...bad);
else Challenge.pass(`n=${deltas.length}, median=${median.toFixed(1)}ms, std=${std.toFixed(1)}ms`);
});
</script>
</body>
@@ -0,0 +1,66 @@
<!doctype html>
<meta charset="utf-8">
<title>27 scroll momentum tail</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: scroll the box to its bottom using a real wheel/trackpad gesture.
Page records the <code>deltaY</code> sequence and requires a momentum-like tail:
a peak <code>|deltaY|</code>, then a decreasing decay over several events
(≥4 wheel events whose <code>|deltaY|</code> is strictly less than the previous
by &gt;5%). Constant or single-shot wheel events (one big delta then nothing)
fail.</p>
<div id="box" style="height:200px;overflow:auto;border:1px solid #555;background:#fff;color:#111;padding:8px">
<div style="height:2400px">scroll me ⬇ … continues …</div>
</div>
</main>
<script>
Challenge.init({ id: "scroll-momentum", instructions: "scroll the box to its bottom with a real gesture" });
const box = document.getElementById("box");
const deltas = []; // signed
const tsLog = [];
box.addEventListener("wheel", (e) => {
deltas.push(e.deltaY);
tsLog.push(e.timeStamp);
if (deltas.length > 200) { deltas.shift(); tsLog.shift(); }
Challenge.log("wheel", { dy: e.deltaY, isTrusted: e.isTrusted });
if (!e.isTrusted) Challenge.fail("wheel event isTrusted=false");
}, { passive: true });
box.addEventListener("scroll", () => {
if (box.scrollTop + box.clientHeight < box.scrollHeight - 2) return;
// We hit bottom. Analyze the deltaY trace.
if (deltas.length === 0) return Challenge.fail("reached bottom with zero wheel events");
if (deltas.length < 6) return Challenge.fail(`only ${deltas.length} wheel events (need ≥6 for momentum)`);
const abs = deltas.map(Math.abs);
// All same sign? Trackpad inertia keeps signs consistent.
const allSameSign = deltas.every(d => Math.sign(d) === Math.sign(deltas[0]));
if (!allSameSign) return Challenge.fail("wheel deltaY signs flip — not a continuous scroll gesture");
// All identical magnitudes? Robotic.
const allEqual = abs.every(a => Math.abs(a - abs[0]) < 0.01);
if (allEqual) return Challenge.fail(`all |deltaY|=${abs[0]} — no momentum`);
// Find peak and require decay tail after it.
const peakIdx = abs.indexOf(Math.max(...abs));
const tail = abs.slice(peakIdx);
let decays = 0;
for (let i = 1; i < tail.length; i++) {
if (tail[i] < tail[i-1] * 0.95) decays++;
}
Challenge.log("trace", { abs, peakIdx, decays });
if (decays < 3) return Challenge.fail(`only ${decays} decay steps after peak — no momentum tail`);
// Timestamps within the tail should span ≥150ms (real inertia lasts hundreds of ms).
const tailTs = tsLog.slice(peakIdx);
const span = tailTs[tailTs.length-1] - tailTs[0];
if (span < 100) return Challenge.fail(`momentum tail only spans ${span.toFixed(0)}ms`);
Challenge.pass(`${deltas.length} wheels, peak |dY|=${abs[peakIdx].toFixed(1)}, ${decays} decay steps over ${span.toFixed(0)}ms`);
});
</script>
</body>
@@ -0,0 +1,72 @@
<!doctype html>
<meta charset="utf-8">
<title>28 IntersectionObserver visibility</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: scroll until the hidden <b>TARGET</b> button comes into view, then click it.
Page uses an <code>IntersectionObserver</code> to track when TARGET crosses the
viewport. Real scrolling produces a sequence of intersection updates with
gradually increasing <code>intersectionRatio</code> (0 → 1) across animation
frames. Programmatic teleports (<code>scrollIntoView({behavior:"instant"})</code>,
direct <code>scrollTop=</code>) produce exactly one observer callback with
<code>ratio≈1</code> and no rAF samples in between — fail.</p>
<div id="scroller" style="height:240px;overflow:auto;border:1px solid #555;background:#fff;color:#111">
<div style="height:1600px;padding:8px">keep scrolling …</div>
<button id="target" style="margin:0 auto 1600px;display:block;padding:14px 22px;background:#1f7a1f;color:#fff;border:0;border-radius:6px">TARGET</button>
</div>
</main>
<script>
Challenge.init({ id: "intersection-visibility", instructions: "scroll until TARGET visible, then click" });
const scroller = document.getElementById("scroller");
const target = document.getElementById("target");
const ratioSamples = [];
const rafSamples = [];
const io = new IntersectionObserver((entries) => {
for (const e of entries) ratioSamples.push({ r: e.intersectionRatio, t: performance.now() });
}, { root: scroller, threshold: Array.from({length: 21}, (_,i) => i/20) });
io.observe(target);
// Track rAF ticks during which scroll was happening.
let lastScrollTop = scroller.scrollTop;
let scrollingFrames = 0;
function tick() {
if (scroller.scrollTop !== lastScrollTop) {
scrollingFrames++;
rafSamples.push({ st: scroller.scrollTop, t: performance.now() });
lastScrollTop = scroller.scrollTop;
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
target.addEventListener("click", (e) => {
if (!e.isTrusted) return Challenge.fail("TARGET click isTrusted=false");
// Need the target to actually be in the viewport at click time.
const rect = target.getBoundingClientRect();
const sRect = scroller.getBoundingClientRect();
const inViewport = rect.top < sRect.bottom && rect.bottom > sRect.top;
if (!inViewport) return Challenge.fail("TARGET clicked while still off-screen");
// Require at least a gradient of intersection ratios from low to high.
const lows = ratioSamples.filter(s => s.r > 0 && s.r < 0.3).length;
const mids = ratioSamples.filter(s => s.r >= 0.3 && s.r < 0.7).length;
const highs = ratioSamples.filter(s => s.r >= 0.7).length;
Challenge.log("io", { samples: ratioSamples.length, lows, mids, highs, scrollingFrames });
const bad = [];
if (ratioSamples.length < 4) bad.push(`only ${ratioSamples.length} IntersectionObserver samples (teleport)`);
if (lows === 0) bad.push("no low-ratio IO samples — target appeared instantly");
if (mids === 0) bad.push("no mid-ratio IO samples — no smooth approach");
if (scrollingFrames < 5) bad.push(`only ${scrollingFrames} rAF frames saw scroll motion`);
if (bad.length) Challenge.fail(...bad);
else Challenge.pass(`IO gradient seen: lows=${lows} mids=${mids} highs=${highs}, rafFrames=${scrollingFrames}`);
});
</script>
</body>
@@ -0,0 +1,44 @@
<!doctype html>
<meta charset="utf-8">
<title>29 shadow DOM controls</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: discover controls inside an <code>open</code> Shadow DOM, click the button, then type <code>shadow-ok</code> into the shadow input.</p>
<div id="host"></div>
</main>
<script>
Challenge.init({ id: "shadow-dom-controls", instructions: "click shadow button, type 'shadow-ok' in shadow input" });
const host = document.getElementById("host");
const root = host.attachShadow({ mode: "open" });
root.innerHTML = `
<style>
.card { border:1px solid #555; border-radius:8px; padding:16px; background:#222; display:inline-block; }
button,input { font:16px system-ui; padding:8px 10px; margin:6px; }
</style>
<div class="card" role="group" aria-label="Shadow test controls">
<button id="shadowBtn">Arm shadow form</button>
<input id="shadowInput" aria-label="Shadow value" placeholder="shadow-ok" disabled>
<span id="status">waiting</span>
</div>
`;
const btn = root.getElementById("shadowBtn");
const input = root.getElementById("shadowInput");
const status = root.getElementById("status");
let armed = false;
btn.addEventListener("click", (e) => {
armed = true;
input.disabled = false;
input.focus();
status.textContent = `armed; click trusted=${e.isTrusted}`;
Challenge.log("shadow-click", { isTrusted: e.isTrusted });
});
input.addEventListener("input", (e) => {
Challenge.log("shadow-input", { value: input.value, isTrusted: e.isTrusted });
if (input.value !== "shadow-ok") return;
if (!armed) return Challenge.fail("input reached target value before shadow button click armed the form");
Challenge.pass("shadow button clicked and shadow input filled");
});
</script>
</body>
@@ -0,0 +1,44 @@
<!doctype html>
<meta charset="utf-8">
<title>30 iframe targeting</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: interact with controls inside a same-origin iframe. Type <code>frame-ok</code> then click Submit inside the frame.</p>
<iframe id="ifr" title="same-origin challenge frame" style="width:520px;height:220px;border:1px solid #555;border-radius:8px;background:#fff"></iframe>
</main>
<script>
Challenge.init({ id: "iframe-targeting", instructions: "type 'frame-ok' and click Submit inside iframe" });
const frame = document.getElementById("ifr");
frame.srcdoc = `<!doctype html><meta charset="utf-8">
<style>body{font:16px system-ui;margin:24px;background:#202020;color:#eee}input,button{font:16px system-ui;padding:8px 10px;margin:6px}</style>
<p>Inside frame: fill and submit.</p>
<input id="insideText" aria-label="Frame value" placeholder="frame-ok">
<button id="insideButton">Submit frame</button>
<pre id="log"></pre>
<script>
const input = document.getElementById('insideText');
const btn = document.getElementById('insideButton');
const log = document.getElementById('log');
let focused = false;
input.addEventListener('focus', () => { focused = true; parent.postMessage({ type:'frame-focus' }, '*'); });
input.addEventListener('input', e => parent.postMessage({ type:'frame-input', value: input.value, trusted: e.isTrusted }, '*'));
btn.addEventListener('click', e => {
const payload = { type:'frame-submit', value: input.value, focused, trusted: e.isTrusted };
log.textContent = JSON.stringify(payload, null, 2);
parent.postMessage(payload, '*');
});
<\/script>`;
window.addEventListener("message", (e) => {
if (!e.data || typeof e.data !== "object") return;
Challenge.log("frame-message", e.data);
if (e.data.type !== "frame-submit") return;
const bad = [];
if (e.data.value !== "frame-ok") bad.push(`iframe input value="${e.data.value}" (need frame-ok)`);
if (!e.data.focused) bad.push("iframe input was not focused before submit");
if (bad.length) Challenge.fail(...bad);
else Challenge.pass(`iframe controls reached; submit trusted=${e.data.trusted}`);
});
</script>
</body>
@@ -0,0 +1,30 @@
<!doctype html>
<meta charset="utf-8">
<title>31 file upload</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: attach <code>test-suite/fixtures/pi-chrome-upload.txt</code>. Page reads name and contents from the File object.</p>
<input id="file" type="file" aria-label="Upload fixture file" style="font-size:16px;padding:10px;border:1px solid #555;border-radius:6px">
<pre id="rep" style="font:12px monospace;background:#111;color:#eee;padding:12px;border-radius:6px"></pre>
</main>
<script>
Challenge.init({ id: "file-upload", instructions: "upload fixtures/pi-chrome-upload.txt" });
const input = document.getElementById("file");
const rep = document.getElementById("rep");
input.addEventListener("change", async (e) => {
const f = input.files && input.files[0];
if (!f) return Challenge.fail("change fired but input.files[0] missing");
const text = await f.text();
const info = { name: f.name, size: f.size, type: f.type, trusted: e.isTrusted, text };
rep.textContent = JSON.stringify(info, null, 2);
Challenge.log("file-change", info);
const bad = [];
if (f.name !== "pi-chrome-upload.txt") bad.push(`file.name=${f.name}`);
if (text !== "pi-chrome upload fixture\n") bad.push(`file content=${JSON.stringify(text)}`);
if (bad.length) Challenge.fail(...bad);
else Challenge.pass(`uploaded ${f.name}; change.isTrusted=${e.isTrusted}`);
});
</script>
</body>
@@ -0,0 +1,61 @@
<!doctype html>
<meta charset="utf-8">
<title>32 keyboard tab navigation</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: use keyboard only. Click Start, then Tab through fields, type <code>one</code>, <code>two</code>, <code>three</code>, Tab to Submit, press Enter.</p>
<button id="start">Start here</button>
<form id="f" style="margin-top:16px;display:grid;gap:10px;max-width:360px">
<label>First <input id="a" name="a" autocomplete="off"></label>
<label>Second <input id="b" name="b" autocomplete="off"></label>
<label>Third <input id="c" name="c" autocomplete="off"></label>
<button id="submit" type="submit">Submit</button>
</form>
<pre id="rep" style="font:12px monospace;background:#111;color:#eee;padding:12px;border-radius:6px"></pre>
</main>
<script>
Challenge.init({ id: "keyboard-tab-navigation", instructions: "keyboard-only tab flow; submit with Enter" });
const ids = ["start", "a", "b", "c", "submit"];
const focusLog = [];
const keyLog = [];
const rep = document.getElementById("rep");
for (const id of ids) {
const el = document.getElementById(id);
el.addEventListener("focus", (e) => {
focusLog.push({ id, t: e.timeStamp });
rep.textContent = JSON.stringify({ focusLog, keyLog }, null, 2);
});
el.addEventListener("keydown", (e) => {
keyLog.push({ id, key: e.key, trusted: e.isTrusted, t: e.timeStamp });
});
}
let sawPointerAfterStart = false;
document.addEventListener("pointerdown", (e) => {
if (e.target && e.target.id !== "start") sawPointerAfterStart = true;
});
document.getElementById("start").addEventListener("click", () => setTimeout(() => document.getElementById("start").focus(), 0));
document.getElementById("f").addEventListener("submit", (e) => {
e.preventDefault();
const vals = {
a: document.getElementById("a").value,
b: document.getElementById("b").value,
c: document.getElementById("c").value
};
const bad = [];
if (vals.a !== "one" || vals.b !== "two" || vals.c !== "three") bad.push(`values=${JSON.stringify(vals)}`);
const seq = focusLog.map(x => x.id).join(">");
for (const id of ids) if (!focusLog.some(x => x.id === id)) bad.push(`never focused ${id}`);
const enter = keyLog.find(x => x.id === "submit" && x.key === "Enter");
if (!enter) bad.push("no Enter keydown on submit button");
else if (!enter.trusted) bad.push("Enter on submit isTrusted=false");
const tabCount = keyLog.filter(x => x.key === "Tab").length;
if (tabCount < 4) bad.push(`only ${tabCount} Tab keydowns (need ≥4)`);
if (sawPointerAfterStart) bad.push("pointerdown after Start; flow was not keyboard-only");
Challenge.log("keyboard-submit", { vals, seq, keyLog, sawPointerAfterStart });
if (bad.length) Challenge.fail(...bad);
else Challenge.pass(`keyboard-only focus sequence ${seq}`);
});
</script>
</body>
@@ -0,0 +1,33 @@
<!doctype html>
<meta charset="utf-8">
<title>33 network and console capture</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: click button. Page emits console messages and performs a fetch. Benchmark harness should verify <code>chrome_list_console_messages</code> and <code>chrome_list_network_requests</code> captured them.</p>
<button id="go" style="padding:16px 24px;font-size:18px">Emit console + fetch</button>
<pre id="rep" style="font:12px monospace;background:#111;color:#eee;padding:12px;border-radius:6px"></pre>
</main>
<script>
Challenge.init({ id: "network-console-capture", instructions: "clear capture, click, then inspect console/network tools" });
document.getElementById("go").addEventListener("click", async (e) => {
console.log("pi-chrome-benchmark-console", { clicked: true, trusted: e.isTrusted });
console.warn("pi-chrome-benchmark-warning");
let data;
try {
const res = await fetch("data:application/json,%7B%22ok%22%3Atrue%2C%22source%22%3A%22pi-chrome-benchmark%22%7D");
data = await res.json();
} catch (err) {
return Challenge.fail("fetch failed: " + err.message);
}
document.getElementById("rep").textContent = JSON.stringify(data, null, 2);
Challenge.log("fetch-result", data);
if (data && data.ok && data.source === "pi-chrome-benchmark") {
Challenge.pass("page fetch completed; verify tool-level console/network capture separately");
} else {
Challenge.fail("unexpected fetch payload: " + JSON.stringify(data));
}
});
</script>
</body>
@@ -0,0 +1,28 @@
<!doctype html>
<meta charset="utf-8">
<title>34 dialog handling</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: handle browser dialogs. Click Prompt, enter <code>pi-chrome</code>, accept it; then click Confirm and accept it. Browser automation must expose/dismiss native dialogs.</p>
<button id="promptBtn">Prompt</button>
<button id="confirmBtn">Confirm</button>
<button id="alertBtn">Alert smoke</button>
<pre id="rep" class="code"></pre>
</main>
<script>
Challenge.init({ id: "dialog-handling", instructions: "accept prompt('pi-chrome') and confirm()" });
const state = { prompt: null, confirm: null, alert: false };
function paint(){ rep.textContent = JSON.stringify(state, null, 2); }
function check(){
paint();
if (state.prompt === "pi-chrome" && state.confirm === true) Challenge.pass("prompt and confirm handled with expected values");
}
promptBtn.addEventListener("click", () => { state.prompt = prompt("Enter token", ""); Challenge.log("prompt-result", { value: state.prompt }); check(); });
confirmBtn.addEventListener("click", () => { state.confirm = confirm("Accept benchmark confirm?"); Challenge.log("confirm-result", { value: state.confirm }); check(); });
alertBtn.addEventListener("click", () => { alert("pi-chrome alert smoke"); state.alert = true; Challenge.log("alert-dismissed", {}); paint(); });
window.addEventListener("beforeunload", e => { e.preventDefault(); e.returnValue = "benchmark beforeunload"; });
paint();
</script>
</body>
@@ -0,0 +1,35 @@
<!doctype html>
<meta charset="utf-8">
<title>35 target blank popup</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: click <code>target=_blank</code> link, detect new tab, switch to it, and verify child page reports PASS.</p>
<a id="open" target="_blank" rel="opener" href="35-target-blank-popup.html?child=1" style="font-size:18px">Open child tab</a>
<p id="msg"></p>
</main>
<script>
Challenge.init({ id: "target-blank-popup", instructions: "open child tab and inspect it" });
const qs = new URLSearchParams(location.search);
if (qs.get("child") === "1") {
document.getElementById("msg").textContent = "Child tab opened.";
Challenge.pass("child tab loaded on same origin");
try { localStorage.setItem("pi-chrome-suite:target-blank-popup", JSON.stringify({ id:"target-blank-popup", verdict:"PASS", reason:["✓ child tab loaded"], ts: Date.now() })); } catch {}
} else {
document.getElementById("msg").textContent = "Parent page. Link should open new tab.";
function maybePassFromStorage(raw) {
if (!raw) return;
try {
const v = JSON.parse(raw);
if (v.verdict === "PASS") Challenge.pass("child reported PASS via storage event/localStorage");
} catch {}
}
window.addEventListener("storage", e => {
if (e.key === "pi-chrome-suite:target-blank-popup") maybePassFromStorage(e.newValue);
});
maybePassFromStorage(localStorage.getItem("pi-chrome-suite:target-blank-popup"));
document.getElementById("open").addEventListener("click", e => Challenge.log("popup-click", { trusted: e.isTrusted }));
}
</script>
</body>
@@ -0,0 +1,49 @@
<!doctype html>
<meta charset="utf-8">
<title>36 modal focus trap</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: open modal, use Tab to cycle focus inside modal only, then Escape closes it and focus returns to opener.</p>
<button id="open">Open modal</button>
<div id="modal" role="dialog" aria-modal="true" aria-label="Focus trap modal" hidden style="position:fixed;inset:20%;background:#222;border:2px solid #6cf;border-radius:10px;padding:20px;z-index:5">
<button id="first">First</button>
<input id="middle" aria-label="Modal input">
<button id="last">Last</button>
</div>
</main>
<script>
Challenge.init({ id: "modal-focus-trap", instructions: "open modal, Tab cycles inside, Escape closes" });
const order = [];
const modal = document.getElementById("modal");
const nodes = [first, middle, last];
let escaped = false, modalOpened = false, outsideFocusWhileOpen = false;
for (const el of [open, ...nodes]) el.addEventListener("focus", () => { order.push(el.id); Challenge.log("focus", { id: el.id }); });
document.addEventListener("focusin", e => {
if (modalOpened && !modal.hidden && !modal.contains(e.target)) outsideFocusWhileOpen = true;
});
open.addEventListener("click", () => { modal.hidden = false; modalOpened = true; first.focus(); });
document.addEventListener("keydown", e => {
if (modal.hidden) return;
if (e.key === "Tab") {
const i = nodes.indexOf(document.activeElement);
if (i >= 0) { e.preventDefault(); nodes[(i + (e.shiftKey ? -1 : 1) + nodes.length) % nodes.length].focus(); }
}
if (e.key === "Escape") { modal.hidden = true; escaped = true; open.focus(); queueMicrotask(check); }
});
function hasAdjacent(seq, a, b) {
return seq.some((x, i) => x === a && seq[i + 1] === b);
}
function check(){
const afterOpen = order.slice(order.indexOf("first"));
const bad = [];
if (!escaped) bad.push("Escape did not close modal");
if (document.activeElement !== open) bad.push("focus did not return to opener");
if (!afterOpen.includes("middle") || !afterOpen.includes("last")) bad.push(`focus cycle incomplete: ${afterOpen.join(">")}`);
if (!hasAdjacent(afterOpen, "last", "first") && !hasAdjacent(afterOpen, "first", "last")) bad.push(`no wrap-around focus trap observed: ${afterOpen.join(">")}`);
if (outsideFocusWhileOpen) bad.push("focus escaped outside modal while open");
if (bad.length) Challenge.fail(...bad); else Challenge.pass(`modal focus trap ok: ${afterOpen.join(">")}`);
}
</script>
</body>
@@ -0,0 +1,42 @@
<!doctype html>
<meta charset="utf-8">
<title>37 autocomplete combobox</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: type <code>as</code>, use ArrowDown then Enter to choose <code>Aster Lamp</code> from ARIA combobox.</p>
<label>Product <input id="combo" role="combobox" aria-autocomplete="list" aria-controls="list" aria-expanded="false" autocomplete="off"></label>
<ul id="list" role="listbox" style="border:1px solid #555;max-width:260px;padding:6px;display:none"></ul>
</main>
<script>
Challenge.init({ id: "autocomplete-combobox", instructions: "type 'as', ArrowDown, Enter to choose Aster Lamp" });
const items = ["Aster Lamp", "Aspen Desk", "Cable Kit", "Desk Mat", "Red Mug"];
let active = -1, opened = false, keyPath = [];
combo.addEventListener("input", renderList);
combo.addEventListener("keydown", e => {
keyPath.push(e.key);
const opts = [...list.querySelectorAll('[role="option"]')];
if (e.key === "ArrowDown") { e.preventDefault(); active = Math.min(active + 1, opts.length - 1); paintActive(opts); }
if (e.key === "Enter" && active >= 0 && opts[active]) { e.preventDefault(); combo.value = opts[active].textContent; close(); check(e); }
});
function renderList(){
const q = combo.value.toLowerCase();
const matches = items.filter(x => x.toLowerCase().includes(q));
list.innerHTML = matches.map((x,i)=>`<li role="option" id="opt-${i}" style="padding:4px;cursor:pointer">${x}</li>`).join("");
list.style.display = matches.length ? "block" : "none";
combo.setAttribute("aria-expanded", matches.length ? "true" : "false");
active = -1; opened ||= matches.length > 0;
}
function paintActive(opts){ opts.forEach((o,i)=>o.style.background = i===active ? "#2b4b6b" : ""); if(opts[active]) combo.setAttribute("aria-activedescendant", opts[active].id); }
function close(){ list.style.display="none"; combo.setAttribute("aria-expanded","false"); }
function check(e){
const bad=[];
if (!opened) bad.push("listbox never opened");
if (combo.value !== "Aster Lamp") bad.push(`selected ${combo.value}`);
if (!keyPath.includes("ArrowDown") || !keyPath.includes("Enter")) bad.push(`missing keyboard selection path: ${keyPath.join(",")}`);
if (!e.isTrusted) bad.push("Enter key isTrusted=false");
if (bad.length) Challenge.fail(...bad); else Challenge.pass("ARIA combobox selected via keyboard");
}
</script>
</body>
@@ -0,0 +1,40 @@
<!doctype html>
<meta charset="utf-8">
<title>38 SPA route change</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: navigate within SPA using links/buttons. URL must change via <code>history.pushState</code> without full reload. Then go Back to Settings, forward to Billing, and Save.</p>
<nav><a href="/settings" id="settings">Settings</a> <a href="/settings/billing" id="billing">Billing</a></nav>
<section id="view"></section>
</main>
<script>
Challenge.init({ id: "spa-route-change", instructions: "Settings → Billing → Back → Billing → Save, no full reload" });
let navs = 0, popstates = 0;
window.__spaBootId = window.__spaBootId || Math.random().toString(36).slice(2);
const bootId = window.__spaBootId;
const navigationEntriesAtBoot = performance.getEntriesByType("navigation").length;
function route(path){ history.pushState({}, "", path); navs++; render(); Challenge.log("route", { path, navs }); }
settings.addEventListener("click", e => { e.preventDefault(); route("/settings"); });
billing.addEventListener("click", e => { e.preventDefault(); route("/settings/billing"); });
window.addEventListener("popstate", () => { popstates++; render(); Challenge.log("popstate", { path: location.pathname, popstates }); });
function render(){
if (location.pathname.endsWith("/settings/billing")) view.innerHTML = `<h2>Billing</h2><button id="save">Save billing settings</button>`;
else if (location.pathname.endsWith("/settings")) view.innerHTML = `<h2>Settings</h2><p>Choose Billing.</p>`;
else view.innerHTML = `<h2>Home</h2>`;
const save = document.getElementById("save");
if (save) save.addEventListener("click", e => {
const bad=[];
if (!location.pathname.endsWith("/settings/billing")) bad.push(`wrong route ${location.pathname}`);
if (navs < 2) bad.push(`only ${navs} SPA route changes`);
if (popstates < 1) bad.push("browser Back/popstate was not observed");
if (window.__spaBootId !== bootId) bad.push("window sentinel changed; reload detected");
if (performance.getEntriesByType("navigation").length !== navigationEntriesAtBoot) bad.push("navigation entry count changed; reload suspected");
if (!e.isTrusted) bad.push("save click isTrusted=false");
if (bad.length) Challenge.fail(...bad); else Challenge.pass("SPA pushState flow completed without reload");
});
}
render();
</script>
</body>
@@ -0,0 +1,14 @@
<!doctype html>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'none'">
<title>39 strict CSP fallback</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: page blocks <code>unsafe-eval</code>. Agent should fall back to screenshot/coordinate observation and click the target without relying on snapshot/evaluate.</p>
<p id="hint">The green target is fixed at known viewport coordinates. Use a screenshot, then click inside it.</p>
<button id="cspTarget" aria-label="strict CSP target">CSP TARGET</button>
</main>
<script src="39-strict-csp-fallback.js"></script>
</body>
@@ -0,0 +1,27 @@
Challenge.init({ id: "strict-csp-fallback", instructions: "use screenshot/coordinates; click CSP TARGET" });
const btn = document.getElementById("cspTarget");
btn.style.cssText = [
"position:fixed",
"left:220px",
"top:220px",
"width:180px",
"height:72px",
"font:700 16px system-ui",
"background:#1f7a1f",
"color:white",
"border:0",
"border-radius:10px",
"box-shadow:0 0 0 4px rgba(31,122,31,.25)"
].join(";");
document.getElementById("cspTarget").addEventListener("click", (e) => {
const r = btn.getBoundingClientRect();
const bad = [];
if (!e.isTrusted) bad.push("click isTrusted=false");
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) {
bad.push(`click coordinates ${e.clientX},${e.clientY} outside target rect ${Math.round(r.left)},${Math.round(r.top)},${Math.round(r.right)},${Math.round(r.bottom)}`);
}
if (bad.length) Challenge.fail(...bad);
else Challenge.pass("strict CSP page completed via trusted viewport click");
});
@@ -0,0 +1,41 @@
<!doctype html>
<meta charset="utf-8">
<title>40 dynamic wait readiness</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: wait until an async control exists and is enabled, then click it. Direct early clicks or fixed sleeps should not be required.</p>
<div id="status" role="status">Loading async action…</div>
<div id="mount" style="margin-top:20px"></div>
</main>
<script>
Challenge.init({ id: "dynamic-wait-readiness", instructions: "wait for async button, then click" });
const start = performance.now();
setTimeout(() => {
const btn = document.createElement("button");
btn.id = "readyAction";
btn.textContent = "Run ready action";
btn.disabled = true;
btn.style.cssText = "padding:14px 22px;font-size:16px;background:#1f7a1f;color:#fff;border:0;border-radius:6px";
document.getElementById("mount").appendChild(btn);
document.getElementById("status").textContent = "Button mounted; enabling soon…";
Challenge.log("mounted", { at: performance.now() - start });
setTimeout(() => {
btn.disabled = false;
btn.dataset.ready = "true";
document.getElementById("status").textContent = "Ready.";
Challenge.log("enabled", { at: performance.now() - start });
}, 450);
btn.addEventListener("click", (e) => {
const elapsed = performance.now() - start;
const bad = [];
if (!e.isTrusted) bad.push("click isTrusted=false");
if (btn.disabled || btn.dataset.ready !== "true") bad.push("clicked before enabled/ready");
if (elapsed < 650) bad.push(`clicked too early at ${elapsed.toFixed(0)}ms`);
if (bad.length) Challenge.fail(...bad);
else Challenge.pass(`waited for async ready state (${elapsed.toFixed(0)}ms)`);
});
}, 350);
</script>
</body>
@@ -0,0 +1,44 @@
<!doctype html>
<meta charset="utf-8">
<title>41 tab lifecycle</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: create a new tab, inspect it, close it, return to parent, then verify lifecycle state.</p>
<p id="msg"></p>
<button id="verify">Verify tab lifecycle</button>
</main>
<script>
Challenge.init({ id: "tab-lifecycle", instructions: "new tab → inspect child → close child → return parent → Verify" });
const qs = new URLSearchParams(location.search);
const key = "pi-chrome-suite:tab-lifecycle:state";
function readState(){ try { return JSON.parse(localStorage.getItem(key) || "{}"); } catch { return {}; } }
function replaceState(next){ localStorage.setItem(key, JSON.stringify({ ...next, ts: Date.now() })); }
function writeState(patch){ localStorage.setItem(key, JSON.stringify({ ...readState(), ...patch, ts: Date.now() })); }
if (qs.get("child") === "1") {
document.title = "[CHILD] 41 tab lifecycle";
document.getElementById("msg").textContent = "Child tab opened. Close this tab, then return to parent.";
writeState({ opened: true, childUrl: location.href, childTitle: document.title });
window.addEventListener("pagehide", () => writeState({ closed: true, closedAt: Date.now() }));
window.addEventListener("beforeunload", () => writeState({ closed: true, closedAt: Date.now() }));
// Do not call Challenge.pass here: parent verification must be the only suite PASS.
Challenge.log("child-loaded", { url: location.href });
} else {
document.getElementById("msg").textContent = "Parent tab. Runner should open this same file with ?child=1 in a new tab, inspect it, close it, then return here.";
replaceState({ parentLoaded: true, closed: false });
document.getElementById("verify").addEventListener("click", (e) => {
const s = readState();
const bad = [];
if (!e.isTrusted) bad.push("verify click isTrusted=false");
if (!s.opened) bad.push("child tab did not record opened=true");
if (!s.childUrl || !s.childUrl.includes("child=1")) bad.push("child URL not recorded");
if (!s.closed) bad.push("child tab close/pagehide was not recorded");
Challenge.log("tab-state", s);
if (bad.length) Challenge.fail(...bad);
else Challenge.pass("new/inspect/close/return tab lifecycle completed");
});
}
</script>
</body>
@@ -0,0 +1,16 @@
<!doctype html>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'none'">
<title>42 strict CSP evaluate/snapshot</title>
<link rel="stylesheet" href="../_style.css">
<script src="../_lib.js"></script>
<body>
<main>
<p>Goal: this page ships a strict CSP (<code>script-src 'self'</code>, no <code>unsafe-eval</code>), which blocks <code>eval</code>/<code>new Function</code>. <code>chrome_evaluate</code> and <code>chrome_snapshot</code> must still work because they run through CDP, which is not subject to page CSP.</p>
<p id="hint">A secret token is exposed only at <code>window.__cspToken</code> — it is never written into the DOM. Use <code>chrome_evaluate</code> to read it, type it into the field (snapshot/uid to find the field), then click Verify.</p>
<label for="tokenInput">Token:</label>
<input id="tokenInput" type="text" autocomplete="off" aria-label="csp token">
<button id="verify" aria-label="verify token">Verify</button>
</main>
<script src="42-strict-csp-evaluate.js"></script>
</body>
@@ -0,0 +1,21 @@
Challenge.init({
id: "strict-csp-evaluate",
instructions: "under strict CSP: read window.__cspToken via chrome_evaluate, type it into the field, click Verify",
});
// Secret available only via JS evaluation. It is intentionally NOT rendered into the DOM and
// is defined non-enumerable, so the only way to obtain it is to evaluate window.__cspToken in
// the page (which proves chrome_evaluate works despite script-src 'self' blocking eval).
const token = "csp-" + Math.random().toString(36).slice(2, 10);
Object.defineProperty(window, "__cspToken", { value: token, enumerable: false, configurable: false, writable: false });
document.getElementById("verify").addEventListener("click", (e) => {
const bad = [];
if (!e.isTrusted) bad.push("verify click isTrusted=false (use trusted/CDP input)");
const val = (document.getElementById("tokenInput").value || "").trim();
if (val !== token) {
bad.push(`token mismatch: got "${val}" expected "${token}" — chrome_evaluate must read window.__cspToken under strict CSP`);
}
if (bad.length) Challenge.fail(...bad);
else Challenge.pass("strict CSP: chrome_evaluate read the hidden token via CDP and trusted input submitted it");
});