/** * Browser regression for the built live-counter artifact. Every API request is * intercepted and fulfilled locally; this script never writes production data. */ import assert from 'node:assert/strict'; import http from 'node:http'; import net from 'node:net'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { setTimeout as delay } from 'node:timers/promises'; const browserName = process.argv.find((argument) => argument.startsWith('--browser='))?.split('=')[1] ?? 'chromium'; assert.ok(['chromium', 'firefox'].includes(browserName)); const moduleName = process.env.PLAYWRIGHT_MODULE ? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href : 'playwright'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const reserve = net.createServer(); await new Promise((resolve, reject) => { reserve.once('error', reject); reserve.listen(0, '127.0.0.1', resolve); }); const port = reserve.address().port; await new Promise((resolve) => reserve.close(resolve)); const origin = `http://127.0.0.1:${port}`; const apiOrigin = 'https://api.azinstitute4autism.com'; const calls = []; let views = 100; let likes = 5; let browser; let preview; try { const drivers = await import(moduleName); const driver = drivers[browserName]; const executablePath = browserName === 'chromium' ? process.env.CHROMIUM_EXECUTABLE : process.env.FIREFOX_EXECUTABLE; browser = await driver.launch(executablePath ? { headless: true, executablePath } : { headless: true }); preview = spawn(process.execPath, ['node_modules/astro/bin/astro.mjs', 'preview', '--host', '127.0.0.1', '--port', String(port)], { cwd: root, env: { ...process.env, ASTRO_TELEMETRY_DISABLED: '1' }, stdio: 'ignore', }); const reachable = () => new Promise((resolve) => { const request = http.get(`${origin}/`, (response) => { response.resume(); resolve(response.statusCode === 200); }); request.setTimeout(500, () => request.destroy()); request.on('error', () => resolve(false)); }); for (let attempt = 0; attempt < 60 && !(await reachable()); attempt += 1) await delay(250); assert.equal(await reachable(), true, 'Loopback Astro preview did not start'); const context = await browser.newContext(); await context.route(`${apiOrigin}/**`, async (route) => { const request = route.request(); const url = new URL(request.url()); if (request.method() === 'OPTIONS') { await route.fulfill({ status: 204, headers: { 'access-control-allow-origin': origin, 'access-control-allow-methods': 'POST, DELETE, OPTIONS', 'access-control-allow-headers': 'content-type', } }); return; } const body = request.postDataJSON?.() ?? undefined; calls.push({ method: request.method(), path: url.pathname, body }); let response; if (url.pathname === '/stats/batch') { response = Object.fromEntries(body.slugs.map((slug) => [slug, { likes, views }])); } else if (url.pathname === '/views') { views += 1; response = { views }; } else if (url.pathname === '/likes' && request.method() === 'POST') { likes += 1; response = { count: likes }; } else if (url.pathname.startsWith('/likes/') && request.method() === 'DELETE') { likes = Math.max(0, likes - 1); response = { count: likes }; } else { await route.fulfill({ status: 404, body: '{}' }); return; } await route.fulfill({ status: 200, contentType: 'application/json', headers: { 'access-control-allow-origin': origin }, body: JSON.stringify(response), }); }); const page = await context.newPage(); const article = '/library/aba-school-readiness-guide'; await page.goto(`${origin}${article}`); await page.locator('[data-counter][data-track-view="true"] [data-views]').waitFor(); await assert.doesNotReject(async () => page.waitForFunction(() => document.querySelector('[data-counter][data-track-view="true"] [data-views]')?.textContent === '101')); assert.equal(calls.filter((call) => call.path === '/views').length, 1); assert.deepEqual(calls.find((call) => call.path === '/views').body, { slug: 'aba-school-readiness-guide' }); await page.locator('[data-counter][data-track-view="true"] [data-like]').click(); await page.waitForFunction(() => document.querySelector('[data-counter][data-track-view="true"] [data-like]')?.getAttribute('aria-pressed') === 'true'); assert.equal(calls.filter((call) => call.path === '/likes' && call.method === 'POST').length, 1); assert.equal(await page.evaluate(() => localStorage.length), 1); assert.deepEqual(await page.evaluate(() => Object.keys(localStorage)), ['aia-liked-articles-v1']); await page.locator('[data-counter][data-track-view="true"] [data-like]').click(); await page.waitForFunction(() => document.querySelector('[data-counter][data-track-view="true"] [data-like]')?.getAttribute('aria-pressed') === 'false'); assert.equal(calls.filter((call) => call.method === 'DELETE').length, 1); await page.reload(); await page.waitForFunction(() => document.querySelector('[data-counter][data-track-view="true"] [data-views]')?.textContent === '102'); assert.equal(calls.filter((call) => call.path === '/views').length, 2, 'a real reload is a new view'); const beforeListing = calls.filter((call) => call.path === '/views').length; await page.goto(`${origin}/library`); await page.waitForFunction(() => document.querySelector('[data-counter] [data-views]')?.textContent !== '—'); assert.equal(calls.filter((call) => call.path === '/views').length, beforeListing, 'listing cards never report views'); for (const route of [ '/es/library/executive-functioning-skills-autism', '/ar/library/executive-functioning-skills-autism', ]) { await page.goto(`${origin}${route}`); await page.waitForFunction(() => document.querySelector('[data-counter][data-track-view="true"]')?.dataset.viewReported === 'true'); } const translated = calls.filter((call) => call.path === '/views').slice(-2); assert.deepEqual(translated.map((call) => call.body.slug), [ 'executive-functioning-skills-autism', 'executive-functioning-skills-autism', ]); console.log(JSON.stringify({ browser: browserName, version: browser.version(), checks: 18, productionRequests: 0 })); await context.close(); } finally { if (preview) preview.kill('SIGTERM'); if (browser) await browser.close(); }