aboutsummaryrefslogtreecommitdiff
path: root/test-suite/challenges
diff options
context:
space:
mode:
authorTianren Dong <tianrendong.dtr@gmail.com>2026-05-13 07:29:41 -0400
committerTianren Dong <tianrendong.dtr@gmail.com>2026-05-13 07:29:41 -0400
commit4c54b05dc15b81e449dc8ac53da8bae536fa2798 (patch)
treef81c5e627dcc4dd81d2fe2fd3b2b584e72a73eca /test-suite/challenges
parent5951b3fb04aaf53b875afe265a57a944d6fce641 (diff)
pi-chrome: snapshot pre-migration state
Diffstat (limited to 'test-suite/challenges')
-rw-r--r--test-suite/challenges/34-dialog-handling.html28
-rw-r--r--test-suite/challenges/35-target-blank-popup.html35
-rw-r--r--test-suite/challenges/36-modal-focus-trap.html49
-rw-r--r--test-suite/challenges/37-autocomplete-combobox.html42
-rw-r--r--test-suite/challenges/38-spa-route-change.html40
5 files changed, 194 insertions, 0 deletions
diff --git a/test-suite/challenges/34-dialog-handling.html b/test-suite/challenges/34-dialog-handling.html
new file mode 100644
index 0000000..25845a7
--- /dev/null
+++ b/test-suite/challenges/34-dialog-handling.html
@@ -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>
diff --git a/test-suite/challenges/35-target-blank-popup.html b/test-suite/challenges/35-target-blank-popup.html
new file mode 100644
index 0000000..0813625
--- /dev/null
+++ b/test-suite/challenges/35-target-blank-popup.html
@@ -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>
diff --git a/test-suite/challenges/36-modal-focus-trap.html b/test-suite/challenges/36-modal-focus-trap.html
new file mode 100644
index 0000000..811207c
--- /dev/null
+++ b/test-suite/challenges/36-modal-focus-trap.html
@@ -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>
diff --git a/test-suite/challenges/37-autocomplete-combobox.html b/test-suite/challenges/37-autocomplete-combobox.html
new file mode 100644
index 0000000..91f35c2
--- /dev/null
+++ b/test-suite/challenges/37-autocomplete-combobox.html
@@ -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>
diff --git a/test-suite/challenges/38-spa-route-change.html b/test-suite/challenges/38-spa-route-change.html
new file mode 100644
index 0000000..533c38d
--- /dev/null
+++ b/test-suite/challenges/38-spa-route-change.html
@@ -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>