/** * Integration tests: load the real page in a same-origin iframe and drive it like a user. * Each test gets a fresh page and an empty bag. */ import { STORAGE_KEY } from "../dist/js/cart.js"; import { assert, equal, sleep, test } from "./harness.js"; async function withApp(fn, { search = "", stored, width = 1200 } = {}) { localStorage.removeItem(STORAGE_KEY); if (stored !== undefined) { localStorage.setItem(STORAGE_KEY, typeof stored === "string" ? stored : JSON.stringify(stored)); } const frame = document.createElement("iframe"); frame.style.width = `${width}px`; const loaded = new Promise((resolve) => frame.addEventListener("load", resolve, { once: true })); frame.src = `../dist/index.html${search}`; document.body.append(frame); await loaded; const { contentWindow: win, contentDocument: doc } = frame; const $ = (selector) => doc.querySelector(selector); const app = { win, doc, $, $$: (selector) => [...doc.querySelectorAll(selector)], click(target) { const el = typeof target === "string" ? $(target) : target; assert(el, `nothing matches ${target}`); el.click(); }, names: () => [...doc.querySelectorAll(".product h3")].map((h) => h.textContent), prices: () => [...doc.querySelectorAll(".product .price")].map((p) => p.textContent), count: () => $("#count").textContent, stored: () => JSON.parse(localStorage.getItem(STORAGE_KEY)), addButton: (index = 0) => doc.querySelectorAll(".product [data-action=add]")[index], }; try { await fn(app); } finally { frame.remove(); localStorage.removeItem(STORAGE_KEY); } } const scenario = (name, fn, options) => test(`app: ${name}`, () => withApp(fn, options)); /* ------------------------------------------------------------------ browsing */ scenario("renders the whole catalogue with sensible defaults", async (a) => { equal(a.names(), [ "Ridgeline Pack", "Camp Enamel Mug", "Switchback Cap", "Alpine Flask", "Afterglow Lantern", "Meadow Blanket", ]); equal(a.prices(), ["$86", "$24", "$32", "$38", "$54", "$68"]); equal(a.$("#results").textContent, "6 products shown"); equal(a.count(), "0"); equal(a.$("#filters [aria-pressed=true]").textContent, "All"); }); scenario("filtering updates the grid, pressed state, announcement and URL", async (a) => { a.click('#filters [data-value="Camp"]'); equal(a.names(), ["Camp Enamel Mug", "Afterglow Lantern", "Meadow Blanket"]); equal(a.$("#results").textContent, "3 products shown"); equal(a.$$("#filters [aria-pressed=true]").map((b) => b.textContent), ["Camp"]); equal(a.win.location.search, "?category=Camp"); a.click('#filters [data-value="All"]'); equal(a.win.location.search, "", "default view leaves the URL clean"); }); scenario("sorting by price reorders the grid and updates the URL", async (a) => { a.$("#sort").value = "low"; a.$("#sort").dispatchEvent(new Event("change", { bubbles: true })); equal(a.prices(), ["$24", "$32", "$38", "$54", "$68", "$86"]); equal(a.win.location.search, "?sort=low"); a.$("#sort").value = "high"; a.$("#sort").dispatchEvent(new Event("change", { bubbles: true })); equal(a.prices(), ["$86", "$68", "$54", "$38", "$32", "$24"]); }); scenario("the view is restored from the URL (shareable links)", async (a) => { equal(a.names(), ["Switchback Cap"]); equal(a.$("#sort").value, "high"); equal(a.$("#filters [aria-pressed=true]").textContent, "Wear"); }, { search: "?category=Wear&sort=high" }); scenario("garbage in the URL is ignored", async (a) => { equal(a.names().length, 6); equal(a.$("#sort").value, "featured"); }, { search: "?category=