// Headless browser walkthrough. Usage: NODE_PATH= node tests/browser_e2e.js http://127.0.0.1:8795 [screenshot_dir] const puppeteer = require("puppeteer-core"); const BASE = process.argv[2] || "http://127.0.0.1:8795"; const SHOTS = process.argv[3] || ""; const results = []; const check = (name, ok, d = "") => { results.push(ok); console.log((ok ? "PASS " : "FAIL ") + name + (d ? ` [${d}]` : "")); }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); (async () => { const browser = await puppeteer.launch({ executablePath: process.env.CHROME || "/usr/bin/google-chrome", headless: "new", args: ["--no-sandbox", "--disable-gpu", "--user-data-dir=/tmp/am-pptr-profile"] }); const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 860 }); const errors = []; page.on("pageerror", (e) => errors.push(e.message)); await page.goto(BASE + "/", { waitUntil: "networkidle0" }); await page.waitForSelector(".card", { timeout: 15000 }); check("landing renders agent-chosen cards", (await page.$$(".card")).length >= 4); check("React app mounted in #root", await page.evaluate(() => !!document.querySelector("#root > .testbar") && !!document.querySelector("#root main#page #stage [data-component=cards]"))); const badge = await page.$eval("#agentBadge", (n) => n.textContent); check("agent mode badge shown", /fallback|built-in|AI:|LLM|Local model/.test(badge), badge); // ask(): type into the chat popup and wait for the agent's reply (LLM replies take a few seconds) const ask = async (text) => { const n = (await page.$$(".msg.bot")).length; await page.type("#chatInput", text); await page.keyboard.press("Enter"); await page.waitForFunction((n) => document.querySelectorAll(".msg.bot").length > n, { timeout: 60000 }, n); await sleep(300); }; // chat search await page.click("#chatLauncher"); await ask("show me bikes"); const titles = await page.$$eval("#stage .card h3", (ns) => ns.map((n) => n.textContent)); check("chat search renders bike cards", titles.length > 0 && titles.every((t) => /bike/i.test(t)), titles.join(" | ")); if (SHOTS) { await sleep(600); await page.screenshot({ path: SHOTS + "/1-landing-chat-search.png" }); } const feeBps = (await (await fetch(BASE + "/api/status")).json()).fee_bps; const money = (c) => "$" + (c / 100).toFixed(2); // online checkout: first card that offers online payment const pick = await page.evaluate(() => { const c = [...document.querySelectorAll("#stage .card")].find((c) => c.querySelector("button.btn:not(.cash)")); return c && { id: c.dataset.listing, price: c.querySelector(".price").childNodes[0].textContent }; }); check("an online-payable listing is visible", !!pick, pick && pick.price); const priceC = Math.round(parseFloat(pick.price.slice(1)) * 100), feeC = Math.round(priceC * feeBps / 10000); await page.click(`.card[data-listing="${pick.id}"] button.btn:not(.cash)`); await page.waitForSelector("[data-component=checkout]", { timeout: 15000 }); const fee = await page.$eval("[data-testid=fee]", (n) => n.textContent); const total = await page.$eval("[data-testid=total]", (n) => n.textContent); check(`checkout shows ${feeBps / 100}% fee and total`, fee === money(feeC) && total === money(priceC + feeC), `price ${pick.price} fee ${fee} total ${total}`); if (SHOTS) { await sleep(600); await page.screenshot({ path: SHOTS + "/2-checkout-fee.png" }); } await page.click("[data-component=checkout] button[type=submit]"); await page.waitForSelector("[data-component=receipt]", { timeout: 15000 }); const rfee = await page.$eval("[data-testid=receipt-fee]", (n) => n.textContent); check("test payment confirms, receipt shows fee", rfee === money(feeC), rfee); // cash meetup: first card offering cash await ask("show me everything"); const cashId = await page.evaluate(() => { const c = [...document.querySelectorAll("#stage .card")].find((c) => c.querySelector("button.cash")); return c && c.dataset.listing; }); await page.click(`.card[data-listing="${cashId}"] button.cash`); await page.waitForSelector("[data-component=cash-meetup]", { timeout: 15000 }); const cfee = await page.$eval("[data-testid=cash-fee]", (n) => n.textContent); check("cash meetup shows $0 fee", cfee === "$0.00", cfee); if (SHOTS) { await sleep(600); await page.screenshot({ path: SHOTS + "/3-cash-meetup.png" }); } await page.click("[data-component=cash-meetup] button.cash"); await page.waitForSelector("[data-component=receipt]", { timeout: 15000 }); check("cash deal completed with $0 fee", (await page.$eval("[data-testid=receipt-fee]", (n) => n.textContent)) === "$0.00"); // sell await page.click("nav [data-event=sell]"); await page.waitForSelector("[data-component=listing-form]", { timeout: 15000 }); await page.type("[name=title]", "Test-only bookshelf"); await page.$eval("[name=price]", (n) => (n.value = "")); await page.type("[name=price]", "30"); await page.type("[name=description]", "Fake listing created by the browser test"); await page.click("[data-component=listing-form] button[type=submit]"); await page.waitForFunction(() => [...document.querySelectorAll("#stage .card h3")].some((h) => h.textContent === "Test-only bookshelf"), { timeout: 15000 }); check("listing form publishes a new card", true); // compare await ask("compare the two tutoring options"); await page.waitForSelector("[data-component=compare]", { timeout: 15000 }); check("compare view renders", (await page.$$("[data-component=compare] th")).length >= 3); // UI copy edit: the user's exact phrasing renames the top-left logo; then reset the look await ask("replace agent market to new internet on the top left corner"); const logo = await page.$eval("#logoText", (n) => n.textContent); check("copy edit: top-left logo says New Internet", logo === "New Internet", logo); if (SHOTS) { await page.evaluate(() => window.scrollTo(0, 0)); await sleep(600); await page.screenshot({ path: SHOTS + "/11-logo-new-internet.png" }); } await ask("reset the look"); check("reset restores the logo", (await page.$eval("#logoText", (n) => n.textContent)) === "Agent Market"); // compare via card checkboxes + compare bar await ask("show me everything"); await page.$$eval("#stage .card input[aria-label=compare]", (ns) => ns.slice(0, 2).forEach((n) => n.click())); const bar = await page.$eval(".cmpbar", (n) => ({ t: n.textContent, d: getComputedStyle(n).display })); await page.click(".cmpbar button"); await page.waitForFunction(() => document.querySelectorAll("[data-component=compare] tr:first-child th").length === 3, { timeout: 15000 }).catch(() => {}); check("compare bar: 2 selected -> compare view", bar.d === "flex" && /^2 selected/.test(bar.t) && (await page.$$("[data-component=compare] tr:first-child th")).length === 3, bar.t); // chat popup close / reopen await page.click("#chatClose"); const closed = await page.evaluate(() => document.getElementById("chat").classList.contains("hidden") && getComputedStyle(document.getElementById("chatLauncher")).display !== "none"); await page.click("#chatLauncher"); const reopened = await page.evaluate(() => !document.getElementById("chat").classList.contains("hidden") && document.querySelectorAll("#log .msg").length > 5); check("chat popup closes and reopens with history", closed && reopened); check("no page JS errors", errors.length === 0, errors.join("; ")); await browser.close(); const passed = results.filter(Boolean).length; console.log(`\n${passed}/${results.length} passed`); process.exit(passed === results.length ? 0 : 1); })().catch((e) => { console.error(e); process.exit(2); });