← back to Small Business Builder
_qa/click-through.mjs
176 lines
// Browserbase click-through QA on https://builds.agentabrams.com/builds
// Captures 10 screenshots + console errors + network anomalies + final findings.
//
// Run: node _qa/click-through.mjs
import 'dotenv/config';
import dotenv from 'dotenv';
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { chromium } from 'playwright-core';
import Browserbase from '@browserbasehq/sdk';
dotenv.config({ path: path.join(os.homedir(), '.claude/skills/browserbase/.env') });
const PROJECT = process.env.BROWSERBASE_PROJECT_ID;
const KEY = process.env.BROWSERBASE_API_KEY;
if (!PROJECT || !KEY) { console.error('missing browserbase env'); process.exit(1); }
const URL = 'https://builds.agentabrams.com/builds';
const AUTH_B64 = Buffer.from('admin:DWSecure2024!').toString('base64');
const OUT = path.join(process.cwd(), '_qa', '2026-05-05-builds');
fs.mkdirSync(OUT, { recursive: true });
const findings = [];
const consoleErrs = [];
const nav = [];
const bb = new Browserbase({ apiKey: KEY });
let trackedEnd = () => {};
async function startSession() {
try {
const helper = await import(path.join(os.homedir(), '.claude/skills/browserbase/track-session.js'));
const wrapped = await helper.trackedSession(bb, PROJECT, { app: 'sbb-qa-builds', note: URL });
trackedEnd = wrapped.end;
return wrapped.session;
} catch {
return bb.sessions.create({ projectId: PROJECT });
}
}
(async () => {
const session = await startSession();
console.log(`session=${session.id}\nlive=${session.connectUrl}`);
console.log(`debug→ ${session.seleniumRemoteUrl ? '' : ''}https://www.browserbase.com/sessions/${session.id}`);
const browser = await chromium.connectOverCDP(session.connectUrl);
const ctx = browser.contexts()[0];
const page = ctx.pages()[0] || await ctx.newPage();
page.on('console', msg => {
if (msg.type() === 'error' || msg.type() === 'warning') {
consoleErrs.push({ type: msg.type(), text: msg.text().slice(0, 240) });
}
});
page.on('pageerror', err => consoleErrs.push({ type: 'pageerror', text: String(err.message).slice(0, 240), stack: String(err.stack || '').slice(0, 600) }));
page.on('framenavigated', frame => { if (frame === page.mainFrame()) nav.push(frame.url()); });
await page.setViewportSize({ width: 1440, height: 900 });
// ★ Send basic-auth header on every request so JS-side fetch() inherits creds.
// URL-embedded user:pass@host is stripped by Chromium for sub-resource fetches,
// which is why the drawer's /api/wa/.../peek call was failing.
await page.setExtraHTTPHeaders({ Authorization: 'Basic ' + AUTH_B64 });
async function shoot(label) {
const file = path.join(OUT, `${String(findings.length + 1).padStart(2,'0')}-${label}.png`);
await page.screenshot({ path: file, fullPage: false });
findings.push({ step: label, file: path.basename(file) });
console.log(` ★ ${label} → ${path.basename(file)}`);
}
try {
// 1. login + initial page
console.log('1. login + load /builds');
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForSelector('#grid', { timeout: 15000 });
await shoot('initial');
// 2. type "morning" into filter
console.log('2. type morning');
await page.fill('#filter', 'morning');
await page.waitForTimeout(400);
await shoot('filter-morning');
// 3. click RESET
console.log('3. RESET');
await page.click('#filter-reset');
await page.waitForTimeout(400);
await shoot('after-reset');
// 4. toggle TOP
console.log('4. TOP chip');
await page.click('#filter-top');
await page.waitForTimeout(400);
await shoot('top-chip');
// 5. toggle STALE (auto-deselects TOP)
console.log('5. STALE chip');
await page.click('#filter-stale');
await page.waitForTimeout(400);
await shoot('stale-chip');
// reset before peek so we have a card visible
await page.click('#filter-reset');
await page.waitForTimeout(400);
// 6. click first PEEK button (force click since it's hover-revealed)
console.log('6. PEEK first card');
const firstPeek = page.locator('.peek-btn').first();
if (await firstPeek.count() > 0) {
await firstPeek.click({ force: true });
} else {
console.log(' no .peek-btn found — falling back to clicking first card with analysis_slug');
await page.evaluate(() => {
const c = document.querySelector('.bcard[data-slug]');
if (c) window.openPeek(c.dataset.slug);
});
}
await page.waitForSelector('#drawer.is-open', { timeout: 8000 }).catch(() => findings.push({ step: 'drawer-not-opened', issue: true }));
await page.waitForTimeout(800);
await shoot('drawer-open');
// 7. drawer DEEP-DIVE
console.log('7. drawer DEEP-DIVE');
const ddBtn = page.locator('#drawer-body form[action$="/deep-dive"] button');
if (await ddBtn.count() > 0) {
await ddBtn.click();
await page.waitForTimeout(2200); // give the synthesis + reload time
await shoot('drawer-after-dd');
} else {
findings.push({ step: 'deep-dive-btn-missing', issue: true });
await shoot('drawer-no-dd-btn');
}
// 8. drawer NEXT
console.log('8. drawer NEXT');
await page.click('#drawer-next');
await page.waitForTimeout(800);
await shoot('drawer-next');
// 9. ESC to close
console.log('9. ESC close');
await page.keyboard.press('Escape');
await page.waitForTimeout(400);
await shoot('drawer-closed');
// Capture viewport metrics
const metrics = await page.evaluate(() => ({
title: document.title,
gridCards: document.querySelectorAll('#grid .bcard').length,
visibleCards: Array.from(document.querySelectorAll('#grid .bcard')).filter(c => c.style.display !== 'none').length,
gradeCounts: ['A','B','C','D','F'].map(g => [g, document.querySelectorAll(`.bcard[data-grade="${g}"]`).length]),
pageHeight: document.body.scrollHeight,
pageWidth: document.body.scrollWidth,
}));
findings.push({ step: 'metrics', ...metrics });
console.log('metrics:', metrics);
} catch (e) {
console.error('ERROR:', e.message);
findings.push({ step: 'fatal-error', message: String(e.message).slice(0, 400) });
} finally {
await browser.close();
await trackedEnd();
}
fs.writeFileSync(path.join(OUT, 'findings.json'), JSON.stringify({ findings, consoleErrs, nav }, null, 2));
console.log(`\n=== DONE ===\nfiles: ${OUT}\nconsole-errors: ${consoleErrs.length}\nnav-events: ${nav.length}`);
if (consoleErrs.length) {
console.log('\nConsole errors:');
consoleErrs.slice(0, 10).forEach(e => console.log(` [${e.type}] ${e.text}`));
}
process.exit(0);
})();