import fs from "fs"; import path from "path"; import { spawnSync } from "child_process"; import { chromium } from "playwright"; const baseUrl = process.env.SQUAREMCP_BASE_URL || "https://squaremcp.com"; const profile = process.env.SQUAREMCP_E2E_PROFILE || "desktop"; const screenshotPath = process.env.SQUAREMCP_E2E_SCREENSHOT || `/tmp/squaremcp-e2e-${profile}.png`; const baselinePath = process.env.SQUAREMCP_E2E_BASELINE || path.join(process.cwd(), "product/site/baselines", `${profile}.png`); const diffPath = process.env.SQUAREMCP_E2E_DIFF || `/tmp/squaremcp-e2e-${profile}-diff.png`; const vaultRoot = process.env.OBSIDIAN_VAULT_PATH || "/home/garfield/obsidian/vaults"; function assert(condition, message) { if (!condition) { throw new Error(message); } } function uniqueEmail() { const stamp = Date.now(); return `squaremcp-e2e-${profile}-${stamp}@example.com`; } function getProfileSettings() { if (profile === "mobile") { return { viewport: { width: 390, height: 1180 }, isMobile: true, hasTouch: true, }; } return { viewport: { width: 1440, height: 1400 }, isMobile: false, hasTouch: false, }; } async function runMobileLayoutChecks(page) { const layout = await page.evaluate(() => { const selectors = [ ".topbar-row", ".topbar-actions", "h1", ".lede", ".hero-points", ".actions", ".hero-panel", "#pilot-form", ]; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const scrollWidth = document.documentElement.scrollWidth; const issues = []; for (const selector of selectors) { const element = document.querySelector(selector); if (!element) { issues.push(`missing element: ${selector}`); continue; } const rect = element.getBoundingClientRect(); if (rect.left < -1 || rect.right > viewportWidth + 1) { issues.push(`horizontal overflow: ${selector}`); } if (rect.width <= 0 || rect.height <= 0) { issues.push(`collapsed element: ${selector}`); } } const actionButtons = Array.from(document.querySelectorAll(".actions .button")); if (actionButtons.length >= 2) { const first = actionButtons[0].getBoundingClientRect(); const second = actionButtons[1].getBoundingClientRect(); const overlap = first.left < second.right && first.right > second.left && first.top < second.bottom && first.bottom > second.top; if (overlap) { issues.push("action buttons overlap"); } } return { scrollWidth, viewportWidth, viewportHeight, issues, }; }); assert( layout.scrollWidth <= layout.viewportWidth + 1, `mobile horizontal overflow detected (${layout.scrollWidth} > ${layout.viewportWidth})` ); assert(layout.issues.length === 0, `mobile layout issues: ${layout.issues.join(", ")}`); } function getEasternDateString() { return new Intl.DateTimeFormat("en-CA", { timeZone: "America/New_York", year: "numeric", month: "2-digit", day: "2-digit", }).format(new Date()); } async function waitForVaultWrite(requestId) { const pilotLogPath = path.join(vaultRoot, "SquareMCP", "Pilot Requests.md"); const dailyNotePath = path.join(vaultRoot, "Daily Notes", `${getEasternDateString()}.md`); const timeoutMs = 10000; const intervalMs = 250; const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const pilotLog = fs.existsSync(pilotLogPath) ? fs.readFileSync(pilotLogPath, "utf8") : ""; const dailyLog = fs.existsSync(dailyNotePath) ? fs.readFileSync(dailyNotePath, "utf8") : ""; if (pilotLog.includes(requestId) && dailyLog.includes(requestId)) { return { pilotLogPath, dailyNotePath }; } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } throw new Error(`vault write not observed for request id ${requestId}`); } async function run() { const browser = await chromium.launch({ headless: true, args: [ "--host-resolver-rules=MAP squaremcp.com 104.190.60.129,MAP www.squaremcp.com 104.190.60.129", ], }); const context = await browser.newContext(getProfileSettings()); const page = await context.newPage(); try { const response = await page.goto(baseUrl, { waitUntil: "networkidle" }); assert(response && response.ok(), "page did not load successfully"); assert((await page.title()) === "SquareMCP", "page title mismatch"); await page.click('a.topbar-link[href="#pricing"]'); await page.waitForFunction(() => window.location.hash === "#pricing"); await page.click('a.topbar-link[href="#pilot-form"]'); await page.waitForFunction(() => window.location.hash === "#pilot-form"); if (profile === "mobile") { await runMobileLayoutChecks(page); } const heroVideo = page.locator("#heroAnimation"); await heroVideo.waitFor(); const src = await heroVideo.getAttribute("src"); assert( src && src.includes("squaremcp-hero-loop.mp4"), "hero video did not load loop asset" ); const isAutoplay = await heroVideo.evaluate((el) => el.autoplay); const isLoop = await heroVideo.evaluate((el) => el.loop); const isMuted = await heroVideo.evaluate((el) => el.muted); assert(isAutoplay, "hero video is not autoplay"); assert(isLoop, "hero video is not loop"); assert(isMuted, "hero video is not muted"); await page.screenshot({ path: screenshotPath, fullPage: true }); if (fs.existsSync(baselinePath)) { const compare = spawnSync( process.execPath, ["product/site/compare-screenshot.mjs", screenshotPath, baselinePath, diffPath, "0.035", "0.025"], { stdio: "inherit" } ); assert(compare.status === 0, `visual diff failed for ${profile}`); } const email = uniqueEmail(); await page.fill('input[name="name"]', `SquareMCP ${profile.toUpperCase()} E2E`); await page.fill('input[name="email"]', email); await page.fill('input[name="company"]', `SquareMCP ${profile} Browser Test`); await page.fill('input[name="role"]', "QA"); await page.evaluate(() => { const tagField = document.querySelector('input[name="submission_tag"]'); if (tagField) { tagField.value = "#squaremcp-e2e #squaremcp-test-cleanup"; } }); await page.selectOption('select[name="use_case"]', { label: "Internal support copilot" }); await page.selectOption('select[name="timeline"]', { label: "Within 2 weeks" }); await page.fill('textarea[name="systems"]', "Postgres, internal REST APIs"); await page.fill('textarea[name="requirements"]', "Audit logs and SSO"); const submitResponsePromise = page.waitForResponse( (resp) => resp.url().includes("/api/pilot-request") && resp.request().method() === "POST" ); await page.click('button[type="submit"]'); const submitResponse = await submitResponsePromise; assert(submitResponse.ok(), `pilot submit failed with status ${submitResponse.status()}`); const submitJson = await submitResponse.json(); assert(submitJson.ok === true, "submit response missing ok=true"); assert( typeof submitJson.request_id === "string" && submitJson.request_id.length > 0, "submit response missing request id" ); const outputText = await page.locator("#pilotOutput").textContent(); assert( outputText && outputText.includes("Saved to SquareMCP intake successfully."), "success message missing" ); assert(outputText.includes(submitJson.request_id), "success message missing request id"); const nameValue = await page.locator('input[name="name"]').inputValue(); assert(nameValue === "", "form did not reset after submit"); const vaultTargets = await waitForVaultWrite(submitJson.request_id); const pilotLog = fs.readFileSync(vaultTargets.pilotLogPath, "utf8"); const dailyLog = fs.readFileSync(vaultTargets.dailyNotePath, "utf8"); assert( pilotLog.includes("#squaremcp-e2e #squaremcp-test-cleanup"), "pilot log missing cleanup tag" ); assert( dailyLog.includes("#squaremcp-e2e #squaremcp-test-cleanup"), "daily log missing cleanup tag" ); console.log(`squaremcp product site e2e test (${profile}): PASS`); console.log(`request_id: ${submitJson.request_id}`); console.log(`email: ${email}`); console.log(`screenshot: ${screenshotPath}`); console.log(`baseline: ${baselinePath}`); console.log(`diff: ${diffPath}`); console.log(`pilot_log: ${vaultTargets.pilotLogPath}`); console.log(`daily_log: ${vaultTargets.dailyNotePath}`); } finally { await context.close(); await browser.close(); } } run().catch((error) => { console.error(`squaremcp product site e2e test (${profile}): FAIL: ${error.message}`); process.exit(1); });