225 lines
13 KiB
JavaScript
225 lines
13 KiB
JavaScript
/**
|
|
* Guarded consultation-only Form A browser regression.
|
|
* Run after an explicitly gated isolated-test build. Every Jotform request is
|
|
* fulfilled or aborted locally; no Jotform request is transmitted.
|
|
*/
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs/promises';
|
|
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 root = fileURLToPath(new URL('../', import.meta.url));
|
|
const moduleName = process.env.PLAYWRIGHT_MODULE
|
|
? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href
|
|
: 'playwright';
|
|
const output = await fs.mkdtemp('/tmp/aia-form-a-consultation-');
|
|
const routes = [
|
|
{ route: '/client-consultation', locale: 'en', drawer: false, screenshot: 'client-consultation' },
|
|
{ route: '/es/client-consultation', locale: 'es', drawer: false, screenshot: 'es-client-consultation' },
|
|
{ route: '/schedule-consultation', locale: 'en', drawer: false, screenshot: 'schedule-consultation' },
|
|
{ route: '/aba-therapy', locale: 'en', drawer: true, screenshot: 'aba-therapy' },
|
|
{ route: '/autism-evaluations', locale: 'en', drawer: true, screenshot: 'autism-evaluations' },
|
|
{ route: '/learner-social-club', locale: 'en', drawer: true, screenshot: 'learner-social-club' },
|
|
{ route: '/es/aba-therapy', locale: 'es', drawer: true, screenshot: 'es-aba-therapy' },
|
|
{ route: '/es/autism-evaluations', locale: 'es', drawer: true, screenshot: 'es-autism-evaluations' },
|
|
{ route: '/es/learner-social-club', locale: 'es', drawer: true, screenshot: 'es-learner-social-club' },
|
|
];
|
|
const widths = [320, 390, 768, 1440];
|
|
const evidence = { routes, widths, checks: 0, blocked: [], jotformAttempts: [], screenshots: [], fallbackChecks: [], status: 'failed' };
|
|
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;
|
|
let browser;
|
|
let preview;
|
|
|
|
function expectedUrl(locale) {
|
|
return 'https://form.jotform.com/262946078314059?formContext=consultation&language=' + locale;
|
|
}
|
|
|
|
try {
|
|
const { chromium } = await import(moduleName);
|
|
browser = await chromium.launch(process.env.CHROMIUM_EXECUTABLE
|
|
? { headless: true, executablePath: process.env.CHROMIUM_EXECUTABLE }
|
|
: { 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 i = 0; i < 60 && !(await reachable()); i++) await delay(250);
|
|
assert.equal(await reachable(), true, 'Loopback preview did not become reachable');
|
|
|
|
for (const width of widths) {
|
|
const context = await browser.newContext({
|
|
viewport: { width, height: width < 500 ? 844 : 960 },
|
|
serviceWorkers: 'block',
|
|
});
|
|
await context.addInitScript(() => localStorage.setItem('aia:service-inquiry:auto-suppressed-until', String(Date.now() + 86400000)));
|
|
await context.routeWebSocket('**/*', (socket) => socket.close());
|
|
await context.route('**/*', async (route) => {
|
|
const request = route.request();
|
|
const url = new URL(request.url());
|
|
if (url.origin === origin && ['GET', 'HEAD'].includes(request.method())) return route.continue();
|
|
const item = { origin: url.origin, path: url.pathname, search: url.search, method: request.method() };
|
|
evidence.blocked.push(item);
|
|
if (url.origin === 'https://form.jotform.com') {
|
|
evidence.jotformAttempts.push(item);
|
|
return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>Local consultation fixture</title><p>Local fixture</p>' });
|
|
}
|
|
return route.abort();
|
|
});
|
|
|
|
for (const expected of routes) {
|
|
const page = await context.newPage();
|
|
const attemptsBeforeNavigation = evidence.jotformAttempts.length;
|
|
evidence.checks++;
|
|
const response = await page.goto(origin + expected.route, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
assert.equal(response?.status(), 200, expected.route);
|
|
assert.equal(await page.locator('main h1').count(), 1, expected.route);
|
|
assert.equal(await page.locator('main form').count(), 0, expected.route);
|
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), expected.route + ' has horizontal overflow at ' + width + 'px');
|
|
|
|
const adapter = page.locator('[data-jotform-form-a]');
|
|
const iframe = adapter.locator('iframe');
|
|
const url = expectedUrl(expected.locale);
|
|
assert.equal(await adapter.count(), 1, expected.route);
|
|
assert.equal(await adapter.getAttribute('data-form-context'), 'consultation', expected.route);
|
|
assert.equal(await adapter.getAttribute('data-form-mode'), 'isolated-test', expected.route);
|
|
assert.equal(await adapter.getAttribute('data-form-environment'), 'isolated-test', expected.route);
|
|
assert.equal(await iframe.getAttribute('title'), expected.locale === 'es' ? 'Formulario de consulta de AIA' : 'AIA consultation form', expected.route);
|
|
assert.equal(await iframe.getAttribute('referrerpolicy'), 'no-referrer', expected.route);
|
|
assert.equal(await iframe.getAttribute('sandbox'), 'allow-forms allow-scripts allow-same-origin', expected.route);
|
|
assert.equal(await adapter.locator('[data-jotform-iframe-fallback]').getAttribute('hidden'), '', expected.route);
|
|
|
|
const attemptsBefore = evidence.jotformAttempts.length;
|
|
if (expected.drawer) {
|
|
const opener = page.locator('[data-service-inquiry-trigger]');
|
|
assert.equal(await opener.count(), 1, expected.route);
|
|
assert.equal(await iframe.getAttribute('src'), null, expected.route + ' must defer the iframe source');
|
|
assert.equal(await iframe.getAttribute('data-jotform-iframe-url'), url, expected.route);
|
|
assert.equal(evidence.jotformAttempts.length, attemptsBefore, expected.route + ' must not request Jotform while closed');
|
|
|
|
await opener.focus();
|
|
await page.keyboard.press('Enter');
|
|
const dialog = page.locator('[data-service-inquiry-drawer]');
|
|
await assert.doesNotReject(dialog.waitFor({ state: 'visible', timeout: 3000 }));
|
|
assert.equal(await iframe.getAttribute('src'), url, expected.route);
|
|
await page.waitForTimeout(20);
|
|
assert.equal(evidence.jotformAttempts.length, attemptsBefore + 1, expected.route + ' should request only after opening');
|
|
assert.equal(await page.locator('.service-inquiry-form').count(), 0, expected.route + ' must not expose legacy fields');
|
|
assert.equal(await page.locator('[data-service-inquiry-close]').getAttribute('aria-label'), expected.locale === 'es' ? 'Cerrar el formulario de consulta' : 'Close consultation form');
|
|
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForFunction(() => !document.querySelector('[data-service-inquiry-drawer]')?.open);
|
|
assert.equal(await page.evaluate(() => document.activeElement?.getAttribute('data-service-inquiry-trigger')), expected.route.split('/').at(-1), expected.route + ' restores focus');
|
|
await opener.click();
|
|
await page.waitForTimeout(20);
|
|
assert.equal(await page.locator('[data-jotform-form-a] iframe').count(), 1, expected.route + ' must retain one iframe');
|
|
assert.equal(evidence.jotformAttempts.length, attemptsBefore + 1, expected.route + ' must not duplicate the request on reopen');
|
|
} else {
|
|
assert.equal(await iframe.getAttribute('src'), url, expected.route);
|
|
await page.waitForTimeout(20);
|
|
assert.ok(evidence.jotformAttempts.length >= attemptsBeforeNavigation + 1, expected.route + ' should request the configured iframe');
|
|
}
|
|
|
|
const box = await iframe.boundingBox();
|
|
const minimumHeight = expected.drawer ? width < 500 ? 1312 : 1152 : width < 500 ? 1088 : 960;
|
|
assert.ok(box && box.width > 0 && box.height >= minimumHeight, expected.route + ' has an insufficient consultation frame');
|
|
await page.screenshot({ path: path.join(output, expected.screenshot + '-' + width + '.png'), fullPage: true });
|
|
evidence.screenshots.push(expected.screenshot + '-' + width + '.png');
|
|
await page.close();
|
|
}
|
|
await context.close();
|
|
}
|
|
|
|
const noJavaScript = await browser.newContext({ viewport: { width: 390, height: 844 }, javaScriptEnabled: false, serviceWorkers: 'block' });
|
|
await noJavaScript.route('**/*', async (route) => {
|
|
const request = route.request();
|
|
const url = new URL(request.url());
|
|
if (url.origin === origin && ['GET', 'HEAD'].includes(request.method())) return route.continue();
|
|
const item = { origin: url.origin, path: url.pathname, search: url.search, method: request.method(), noJavaScript: true };
|
|
evidence.blocked.push(item);
|
|
if (url.origin === 'https://form.jotform.com') evidence.jotformAttempts.push(item);
|
|
return route.abort();
|
|
});
|
|
for (const expected of routes) {
|
|
const page = await noJavaScript.newPage();
|
|
evidence.checks++;
|
|
await page.goto(origin + expected.route, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
if (expected.drawer) {
|
|
const fallback = page.locator('.service-inquiry-drawer__noscript');
|
|
assert.equal(await fallback.count(), 1, expected.route + ' no-JS fallback');
|
|
assert.equal(await fallback.isVisible(), true, expected.route + ' no-JS fallback visibility');
|
|
assert.equal(await fallback.locator('a').getAttribute('href'), expectedUrl(expected.locale), expected.route);
|
|
} else {
|
|
const fallback = page.locator('[data-jotform-form-a] .jotform-iframe__fallback--noscript');
|
|
assert.equal(await fallback.count(), 1, expected.route + ' no-JS fallback');
|
|
assert.equal(await fallback.isVisible(), true, expected.route + ' no-JS fallback visibility');
|
|
}
|
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), expected.route + ' no-JS horizontal overflow');
|
|
await page.close();
|
|
}
|
|
await noJavaScript.close();
|
|
|
|
const fallbackContext = await browser.newContext({ viewport: { width: 390, height: 844 }, serviceWorkers: 'block' });
|
|
await fallbackContext.route('**/*', async (route) => {
|
|
const request = route.request();
|
|
const url = new URL(request.url());
|
|
if (url.origin === origin && ['GET', 'HEAD'].includes(request.method())) return route.continue();
|
|
const item = { origin: url.origin, path: url.pathname, search: url.search, method: request.method(), fallback: true };
|
|
evidence.blocked.push(item);
|
|
if (url.origin === 'https://form.jotform.com') {
|
|
evidence.jotformAttempts.push(item);
|
|
// Hold this guarded request rather than aborting it. Some browsers fire
|
|
// iframe load for an aborted/error document, which intentionally clears
|
|
// the fallback because load is only a transport signal.
|
|
return new Promise(() => {});
|
|
}
|
|
return route.abort();
|
|
});
|
|
const fallbackPage = await fallbackContext.newPage();
|
|
await fallbackPage.addInitScript(() => {
|
|
const original = window.setTimeout.bind(window);
|
|
window.setTimeout = (callback, timeout, ...args) => original(callback, timeout === 15000 ? 80 : timeout, ...args);
|
|
});
|
|
await fallbackPage.goto(origin + '/aba-therapy', { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
await fallbackPage.locator('[data-service-inquiry-trigger]').click();
|
|
const fallback = fallbackPage.locator('[data-jotform-iframe-fallback]');
|
|
await fallbackPage.waitForFunction(() => document.querySelector('[data-jotform-iframe-fallback]')?.hidden === false);
|
|
assert.equal(await fallback.isVisible(), true, 'blocked drawer request exposes the hosted fallback after timeout');
|
|
evidence.fallbackChecks.push('drawer-timeout-fallback-visible');
|
|
await fallbackContext.close();
|
|
|
|
assert.ok(evidence.jotformAttempts.length >= routes.length * widths.length, 'Expected intercepted Jotform GET attempts');
|
|
for (const attempt of evidence.jotformAttempts) {
|
|
assert.equal(attempt.method, 'GET');
|
|
assert.equal(attempt.path, '/262946078314059');
|
|
assert.match(attempt.search, /^\?formContext=consultation&language=(?:en|es)$/);
|
|
}
|
|
assert.equal(evidence.blocked.filter((item) => !['GET', 'HEAD'].includes(item.method)).length, 0, 'A non-read request was attempted');
|
|
evidence.status = 'passed';
|
|
} catch (error) {
|
|
evidence.error = error instanceof Error ? error.message : String(error);
|
|
throw error;
|
|
} finally {
|
|
await fs.writeFile(path.join(output, 'evidence.json'), JSON.stringify(evidence, null, 2));
|
|
if (preview && preview.exitCode === null) {
|
|
preview.kill('SIGTERM');
|
|
await Promise.race([new Promise((resolve) => preview.once('exit', resolve)), delay(3000)]);
|
|
if (preview.exitCode === null) preview.kill('SIGKILL');
|
|
}
|
|
await browser?.close();
|
|
console.log(JSON.stringify({ ...evidence, artifactDirectory: output }, null, 2));
|
|
}
|