SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
6.4 KB · 120 lines javascript
Raw Blame History
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