/** * SX Test Runner — drives tests in an iframe, reports results. * Updates [data-test] elements in-place with pass/fail icons. * Expects: #run-btn, #test-status, #test-summary, #test-iframe, [data-test] */ (function() { "use strict"; var TESTS = [ { name: "click-to-load", actions: [ { type: "click", selector: "button[hx-get]" }, { type: "wait", ms: 2000 }, { type: "assert-text", selector: "#click-result", contains: "Content loaded!" } ]}, { name: "click-no-oob-leak", actions: [ { type: "click", selector: "button[hx-get]" }, { type: "wait", ms: 2000 }, { type: "assert-text", selector: "#click-result", notContains: "defcomp" } ]}, { name: "search-debounce", actions: [ { type: "fill", selector: "input[hx-get]", value: "hx-get" }, { type: "wait", ms: 1500 }, { type: "assert-text", selector: "#search-results", contains: "GET request" } ]}, { name: "search-no-results", actions: [ { type: "fill", selector: "input[hx-get]", value: "xyznonexistent" }, { type: "wait", ms: 1500 }, { type: "assert-text", selector: "#search-results", contains: "No results" } ]}, { name: "tab-overview", actions: [ { type: "click", selector: "button[hx-get*='tab=overview']" }, { type: "wait", ms: 2000 }, { type: "assert-text", selector: "#htmx-tab-content", contains: "htmx gives you access" } ]}, { name: "tab-features", actions: [ { type: "click", selector: "button[hx-get*='tab=features']" }, { type: "wait", ms: 2000 }, { type: "assert-text", selector: "#htmx-tab-content", contains: "Any element" } ]}, { name: "append-item", actions: [ { type: "click", selector: "button[hx-post*='api.append']" }, { type: "wait", ms: 2000 }, { type: "assert-count", selector: "#item-list > *", gte: 1 } ]}, { name: "form-submit", actions: [ { type: "fill", selector: "form[hx-post] input[name='name']", value: "Alice" }, { type: "fill", selector: "form[hx-post] input[name='email']", value: "alice@test.com" }, { type: "click", selector: "form[hx-post] button[type='submit']" }, { type: "wait", ms: 2000 }, { type: "assert-text", selector: "#form-result", contains: "Alice" } ]} ]; if (window.__sxTests) TESTS = window.__sxTests; var runBtn, statusEl, summaryEl, iframe; function init() { runBtn = document.getElementById("run-btn"); statusEl = document.getElementById("test-status"); summaryEl = document.getElementById("test-summary"); iframe = document.getElementById("test-iframe"); if (runBtn) { console.log("[sx-test] Runner initialized, " + TESTS.length + " tests"); runBtn.addEventListener("click", function() { console.log("[sx-test] Run clicked"); runAll(); }); } else { console.warn("[sx-test] #run-btn not found"); } } function sleep(ms) { return new Promise(function(r) { setTimeout(r, ms); }); } function waitForReady(doc, timeout) { return new Promise(function(resolve, reject) { var start = Date.now(); (function check() { try { if (doc.documentElement.getAttribute("data-sx-ready") === "true") return resolve(); } catch(e) {} if (Date.now() - start > timeout) return reject(new Error("Timeout")); setTimeout(check, 200); })(); }); } function reloadIframe() { return new Promise(function(resolve) { iframe.addEventListener("load", function onLoad() { iframe.removeEventListener("load", onLoad); resolve(); }); iframe.contentWindow.location.reload(); }); } function updateTestItem(name, state, error) { var item = document.querySelector('[data-test="' + name + '"]'); if (!item) return; var icon = item.querySelector('[data-role="test-icon"]'); if (!icon) return; if (state === "running") { icon.textContent = "⟳"; icon.style.color = "#7c3aed"; item.style.borderColor = "#c4b5fd"; } else if (state === "pass") { icon.textContent = "✓"; icon.style.color = "#16a34a"; item.style.borderColor = "#bbf7d0"; item.style.backgroundColor = "#f0fdf4"; } else if (state === "fail") { icon.textContent = "✗"; icon.style.color = "#dc2626"; item.style.borderColor = "#fecaca"; item.style.backgroundColor = "#fef2f2"; if (error) { var errEl = item.querySelector('[data-role="test-error"]'); if (!errEl) { errEl = document.createElement("div"); errEl.setAttribute("data-role", "test-error"); errEl.style.cssText = "padding:8px 16px;font-size:12px;color:#dc2626;border-top:1px solid #fecaca;background:#fef2f2"; item.querySelector("summary").after(errEl); } errEl.textContent = error; } } } function resetAllItems() { var items = document.querySelectorAll("[data-test]"); for (var i = 0; i < items.length; i++) { var icon = items[i].querySelector('[data-role="test-icon"]'); if (icon) { icon.textContent = "○"; icon.style.color = ""; } items[i].style.borderColor = ""; items[i].style.backgroundColor = ""; var err = items[i].querySelector('[data-role="test-error"]'); if (err) err.remove(); } } async function runOneTest(test) { var doc = iframe.contentDocument; for (var j = 0; j < test.actions.length; j++) { var a = test.actions[j]; if (a.type === "click") { var el = doc.querySelector(a.selector); if (!el) throw new Error("Not found: " + a.selector); el.click(); } else if (a.type === "fill") { var el = doc.querySelector(a.selector); if (!el) throw new Error("Not found: " + a.selector); el.focus(); el.value = ""; el.value = a.value; el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); } else if (a.type === "wait") { await sleep(a.ms); } else if (a.type === "assert-text") { var el = doc.querySelector(a.selector); if (!el) throw new Error("Not found: " + a.selector); var txt = el.textContent || ""; if (a.contains && txt.indexOf(a.contains) === -1) throw new Error("Expected '" + a.contains + "' in '" + txt.substring(0, 60) + "'"); if (a.notContains && txt.indexOf(a.notContains) !== -1) throw new Error("Unexpected '" + a.notContains + "'"); } else if (a.type === "assert-count") { var els = doc.querySelectorAll(a.selector); if (a.gte !== undefined && els.length < a.gte) throw new Error("Expected >=" + a.gte + ", got " + els.length); } } } async function runAll() { resetAllItems(); summaryEl.innerHTML = ""; runBtn.disabled = true; runBtn.style.opacity = "0.5"; var passed = 0, failed = 0; for (var i = 0; i < TESTS.length; i++) { var test = TESTS[i]; statusEl.textContent = test.name + " (" + (i+1) + "/" + TESTS.length + ")"; updateTestItem(test.name, "running"); if (i > 0) await reloadIframe(); try { await waitForReady(iframe.contentDocument, 15000); await sleep(500); } catch(e) { updateTestItem(test.name, "fail", "Page load timeout"); failed++; continue; } try { await runOneTest(test); updateTestItem(test.name, "pass"); passed++; } catch(err) { updateTestItem(test.name, "fail", err.message); failed++; } } runBtn.disabled = false; runBtn.style.opacity = "1"; statusEl.textContent = "Done"; var total = passed + failed; summaryEl.innerHTML = '