← back to Small Business Builder
qa click-through: log session-minutes to cost-tracker via shared helper
783c9b75cadecadb9a4ec26a295ec1e30692ff0a · 2026-05-12 16:53:32 -0700 · Steve
Files touched
Diff
commit 783c9b75cadecadb9a4ec26a295ec1e30692ff0a
Author: Steve <steve@designerwallcoverings.com>
Date: Tue May 12 16:53:32 2026 -0700
qa click-through: log session-minutes to cost-tracker via shared helper
---
_qa/click-through.mjs | 175 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 175 insertions(+)
diff --git a/_qa/click-through.mjs b/_qa/click-through.mjs
new file mode 100644
index 0000000..e553803
--- /dev/null
+++ b/_qa/click-through.mjs
@@ -0,0 +1,175 @@
+// 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);
+})();
← a3c524f tighten .gitignore: add missing standing-rule patterns (dist
·
back to Small Business Builder
·
snapshot: 66 file(s) changed, +54 new, ~12 modified dadcde3 →