spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1/**2 * QA flows (Playwright): live feed → event drawer → company → timeline filter → compare → watchlist add, plus ⌘K search3 * and the admin token gate. Mobile (390) and desktop (1440). Fails on console errors.4 * Run: node qa/flows.mjs [BASE_URL]5 */6import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';7import { mkdirSync } from 'node:fs';89const BASE = process.argv[2] ?? 'http://localhost:8370';10const OUT = new URL('./screens/', import.meta.url).pathname;11mkdirSync(OUT, { recursive: true });12const browser = await chromium.launch();13let failures = 0;14const step = (ok, msg) => {15 if (!ok) failures++;16 console.log(`${ok ? 'OK ' : 'FAIL'} ${msg}`);17};1819for (const width of [390, 1440]) {20 const mobile = width < 768;21 const ctx = await browser.newContext({ viewport: { width, height: mobile ? 844 : 900 }, isMobile: mobile, hasTouch: mobile, colorScheme: 'dark' });22 const page = await ctx.newPage();23 const errors = [];24 page.on('pageerror', (e) => errors.push(String(e)));25 page.on('console', (m) => m.type() === 'error' && !/favicon/.test(m.text()) && errors.push(m.text()));26 const shot = (n) => page.screenshot({ path: `${OUT}flow-${width}-${n}.png` }).catch(() => undefined);27 console.log(`\n— flows @ ${width} —`);2829 // 1. live feed → event drawer30 await page.goto(`${BASE}/live`, { waitUntil: 'networkidle' });31 const rows = await page.locator('[data-live-feed] [data-event-id]').count();32 step(rows > 0, `live feed renders ${rows} rows`);33 await page.locator('[data-live-feed] [data-open-event]').first().click();34 await page.waitForSelector('[data-event-drawer]', { timeout: 5000 }).catch(() => undefined);35 const drawer = await page.locator('[data-event-drawer]').count();36 step(drawer === 1, 'event drawer opens with evidence');37 const hasSources = await page.locator('[data-event-drawer] a[href^="http"]').count();38 step(hasSources > 0, `drawer links to ${hasSources} source URL(s)`);39 await shot('1-drawer');4041 // 2. drawer → company page42 const companyLink = page.locator('[data-event-drawer] a[href^="/company/"]').first();43 const companyHref = await companyLink.getAttribute('href');44 await companyLink.click();45 await page.waitForURL(/\/company\//, { timeout: 15000 });46 await page.waitForLoadState('networkidle');47 step(!!companyHref && page.url().includes(companyHref), `navigated to company ${companyHref}`);48 step((await page.locator('[data-metric-tiles]').count()) === 1, 'metric tiles rendered');49 step((await page.locator('[data-density-strip]').count()) === 1, 'data-density strip rendered');50 await shot('2-company');5152 // 3. timeline tab + filter53 await page.getByRole('tab', { name: 'Timeline' }).click();54 await page.waitForURL(/tab=timeline/, { timeout: 15000 });55 await page.waitForLoadState('networkidle');56 await page.getByRole('tab', { name: 'Jobs', exact: true }).nth(0).click().catch(() => undefined);57 const jobsFilter = page.locator('[role="tablist"][aria-label="Timeline filter"] a', { hasText: 'Jobs' });58 await jobsFilter.click();59 await page.waitForURL(/filter=jobs/, { timeout: 15000 });60 await page.waitForLoadState('networkidle');61 const badges = await page.locator('[role="tabpanel"] [data-event-type]').evaluateAll((els) => [...new Set(els.map((e) => e.getAttribute('data-event-type')))]);62 step(badges.every((b) => b === 'hiring') , `timeline filter=jobs shows only hiring events (${badges.join(',') || 'none'})`);63 await shot('3-timeline');6465 // 4. compare66 await page.goto(`${BASE}/company/compare?companies=stripe,adyen`, { waitUntil: 'networkidle' });67 await page.locator('[data-compare-input]').fill('block');68 await page.waitForSelector('[data-compare-picker] [role="option"]', { timeout: 5000 });69 await page.locator('[data-compare-picker] [role="option"]').first().click();70 await page.waitForURL(/companies=stripe,adyen,block/, { timeout: 15000 });71 await page.waitForLoadState('networkidle');72 step((await page.locator('table thead th').count()) >= 4, 'compare table has three company columns');73 await shot('4-compare');7475 // 5. watchlist add76 await page.goto(`${BASE}/company/stripe`, { waitUntil: 'networkidle' });77 const watch = page.locator('[data-watch="stripe"]');78 await watch.click();79 await page.waitForTimeout(800);80 step((await watch.getAttribute('aria-pressed')) === 'true', 'watch button toggles to Watching');81 await page.goto(`${BASE}/watchlist`, { waitUntil: 'networkidle' });82 await page.waitForTimeout(800);83 step((await page.locator('[data-watchlist] a[href="/company/stripe"]').count()) > 0, 'watchlist page lists Stripe');84 await page.locator('[data-alert-form] input[aria-label="Rule name"]').fill('QA pricing rule');85 await page.locator('[data-alert-form] button[type="submit"]').click();86 await page.waitForTimeout(800);87 step((await page.getByText('QA pricing rule').count()) > 0, 'alert rule created');88 await shot('5-watchlist');8990 // 6. ⌘K search91 await page.goto(`${BASE}/`, { waitUntil: 'networkidle' });92 await page.keyboard.press(process.platform === 'darwin' ? 'Meta+k' : 'Control+k');93 await page.waitForSelector('[data-palette-input]', { timeout: 5000 });94 await page.locator('[data-palette-input]').fill('stri');95 await page.waitForSelector('[data-palette-row]', { timeout: 5000 });96 await page.keyboard.press('Enter');97 await page.waitForURL(/\/company\/stripe|\/search/, { timeout: 15000 });98 step(/\/company\/stripe|\/search/.test(page.url()), `⌘K search navigates (${new URL(page.url()).pathname})`);99 await shot('6-search');100101 // 7. admin gate102 await page.goto(`${BASE}/admin`, { waitUntil: 'networkidle' });103 await page.locator('[data-admin-token]').fill('dev-admin-token');104 await page.locator('[data-admin-token]').press('Enter');105 await page.waitForSelector('h1:has-text("Overview")', { timeout: 10000 }).catch(() => undefined);106 await page.waitForLoadState('networkidle');107 step((await page.locator('h1:has-text("Overview")').count()) === 1, 'admin overview renders after token');108 await page.goto(`${BASE}/admin/sensors?filter=failing`, { waitUntil: 'networkidle' });109 await page.waitForTimeout(800);110 step((await page.locator('table tbody tr').count()) > 0, 'admin sensors table renders');111 await shot('7-admin');112113 step(errors.length === 0, `no console errors (${errors.length})${errors.length ? ' :: ' + errors[0].slice(0, 160) : ''}`);114 await ctx.close();115}116117await browser.close();118console.log(failures ? `\n${failures} failure(s)` : '\nall flows OK');119process.exit(failures ? 1 : 0);120