133 lines
6.4 KiB
JavaScript
133 lines
6.4 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|