[object Object]

← 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 →