Add multi-account OAuth, Obsidian integration, product assets, and test tooling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
257
product/site/e2e-test.mjs
Normal file
257
product/site/e2e-test.mjs
Normal file
@@ -0,0 +1,257 @@
|
||||
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 heroImage = page.locator("#heroAnimation");
|
||||
await heroImage.waitFor();
|
||||
const initialSrc = await heroImage.getAttribute("src");
|
||||
assert(
|
||||
initialSrc && initialSrc.includes("squaremcp_launch.gif"),
|
||||
"hero image did not start with animated asset"
|
||||
);
|
||||
|
||||
const playMs = Number((await heroImage.getAttribute("data-play-ms")) || "0");
|
||||
assert(playMs > 0, "hero animation play duration missing");
|
||||
await page.waitForTimeout(playMs + 750);
|
||||
const finalSrc = await heroImage.getAttribute("src");
|
||||
assert(
|
||||
finalSrc && finalSrc.includes("squaremcp_launch_poster.png"),
|
||||
"hero image did not swap to poster"
|
||||
);
|
||||
|
||||
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.02", "0.015"],
|
||||
{ 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);
|
||||
});
|
||||
Reference in New Issue
Block a user