194 lines
12 KiB
JavaScript
194 lines
12 KiB
JavaScript
/**
|
|
* Deterministic Form A adapter check. Build the requested mode first, then
|
|
* run this against a loopback-only Astro preview. Every external request is
|
|
* intercepted and aborted before it can leave the browser.
|
|
*
|
|
* FORM_A_IFRAME_BROWSER_MODE=isolated-test \
|
|
* PLAYWRIGHT_MODULE=/path/to/playwright/index.mjs \
|
|
* CHROMIUM_EXECUTABLE=/path/to/chromium node tools/jotform-form-a-browser.mjs
|
|
*/
|
|
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 mode = process.env.FORM_A_IFRAME_BROWSER_MODE ?? 'off';
|
|
if (!['off', 'mock', 'isolated-test'].includes(mode)) throw new Error(`Unsupported Form A browser mode: ${mode}`);
|
|
|
|
const expectedIframe = mode === 'isolated-test';
|
|
const routes = [
|
|
{ route: '/contact', locale: 'en', context: 'contact', screenshot: 'contact' },
|
|
{ route: '/es/contact', locale: 'es', context: 'contact', screenshot: 'es-contact' },
|
|
{ route: '/lp/aba-therapy', locale: 'en', context: 'landing', screenshot: 'aba-therapy' },
|
|
{ route: '/lp/aba-insurance', locale: 'en', context: 'landing', screenshot: 'aba-insurance' },
|
|
{ route: '/lp/aba-ahcccs', locale: 'en', context: 'landing', screenshot: 'aba-ahcccs' },
|
|
{ route: '/lp/aba-private-pay', locale: 'en', context: 'landing', screenshot: 'aba-private-pay' },
|
|
{ route: '/lp/early-intervention', locale: 'en', context: 'landing', screenshot: 'early-intervention' },
|
|
];
|
|
const widths = [320, 390, 768, 1440];
|
|
const output = await fs.mkdtemp('/tmp/aia-form-a-iframe-');
|
|
const evidence = { mode, routes, widths, checks: 0, blocked: [], jotformAttempts: [], screenshots: [], 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;
|
|
|
|
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.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.abort();
|
|
});
|
|
const page = await context.newPage();
|
|
for (const expected of routes) {
|
|
const { route, locale, context: formContext, screenshot: screenshotName } = expected;
|
|
evidence.checks++;
|
|
const response = await page.goto(origin + route, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
assert.equal(response?.status(), 200, route);
|
|
assert.equal(await page.locator('main h1').count(), 1, route);
|
|
assert.equal(await page.locator('main form').count(), 0, route);
|
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), `${route} at ${width}px has horizontal overflow`);
|
|
const iframe = page.locator('[data-jotform-form-a] iframe');
|
|
if (!expectedIframe) {
|
|
assert.equal(await iframe.count(), 0, `${route} should not emit an iframe in ${mode}`);
|
|
assert.equal(await page.locator('[data-jotform-form-a]').count(), 0, `${route} should not emit an adapter in ${mode}`);
|
|
continue;
|
|
}
|
|
const expectedUrl = `https://form.jotform.com/262946078314059?formContext=${formContext}&language=${locale}`;
|
|
assert.equal(await iframe.count(), 1, route);
|
|
assert.equal(await iframe.getAttribute('src'), expectedUrl, route);
|
|
assert.equal(await iframe.getAttribute('title'), locale === 'es' ? 'Formulario de consultas generales de AIA' : 'AIA general enquiries form', route);
|
|
assert.equal(await iframe.getAttribute('referrerpolicy'), 'no-referrer', route);
|
|
assert.equal(await iframe.getAttribute('sandbox'), 'allow-forms allow-scripts allow-same-origin', route);
|
|
assert.equal(await page.locator('[data-jotform-form-a][data-form-mode="isolated-test"]').count(), 1, route);
|
|
assert.equal(await page.locator(`[data-jotform-form-a][data-form-context="${formContext}"]`).count(), 1, route);
|
|
const frameBox = await iframe.boundingBox();
|
|
const expectedFrameHeight = formContext === 'contact'
|
|
? width < 500 ? 1088 : 960
|
|
: width < 500 ? 800 : 672;
|
|
assert.ok(frameBox && frameBox.width > 0 && frameBox.height >= expectedFrameHeight, `${route} iframe has an insufficient context-specific responsive frame`);
|
|
if ((route === '/contact' || route === '/es/contact') && width >= 768) {
|
|
const copyBox = await page.locator('.contact-page__copy').boundingBox();
|
|
const adapterBox = await page.locator('[data-jotform-form-a]').boundingBox();
|
|
assert.ok(copyBox && adapterBox && Math.abs(copyBox.width - adapterBox.width) <= 1, `${route} should use the full desktop grid column`);
|
|
}
|
|
const fallbackMarkup = await iframe.evaluate((element) => element.innerHTML);
|
|
assert.match(fallbackMarkup, /class="jotform-iframe__fallback"/, route);
|
|
const encodedFallbackUrl = expectedUrl.replace(/&/g, '&');
|
|
assert.match(fallbackMarkup, new RegExp(`href="${encodedFallbackUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`), route);
|
|
assert.match(fallbackMarkup, /target="_blank" rel="noopener noreferrer"/, route);
|
|
assert.equal(await page.locator('[data-jotform-form-a] > .jotform-iframe__fallback').count(), 0, `${route} should not show a duplicate parent fallback`);
|
|
if (route.startsWith('/lp/')) {
|
|
const expectedSurface = route === '/lp/early-intervention'
|
|
? '#consultation.lead-form > [data-jotform-form-a].jotform-iframe--embedded'
|
|
: '.form-wrap > [data-jotform-form-a].jotform-iframe--embedded';
|
|
assert.equal(await page.locator(expectedSurface).count(), 1, route);
|
|
assert.equal(await page.locator('#lead-form, [data-form-a-preview], #consultation input:enabled').count(), 0, route);
|
|
if (route !== '/lp/early-intervention') {
|
|
assert.equal(
|
|
await page.locator('[data-jotform-form-a] .jotform-iframe__description').innerText(),
|
|
'Use this form to request a conversation. An AIA team member can explain the next steps for your family.',
|
|
`${route} should use the shared iframe description instead of repeating companion copy`,
|
|
);
|
|
}
|
|
}
|
|
await iframe.focus();
|
|
assert.equal(await page.evaluate(() => document.activeElement?.tagName), 'IFRAME', route);
|
|
const screenshot = path.join(output, `${screenshotName}-${width}.png`);
|
|
await page.screenshot({ path: screenshot, fullPage: true });
|
|
evidence.screenshots.push(screenshot);
|
|
}
|
|
await context.close();
|
|
}
|
|
|
|
if (expectedIframe) {
|
|
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, javaScriptEnabled: false, serviceWorkers: 'block' });
|
|
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(), noJavaScript: true };
|
|
evidence.blocked.push(item);
|
|
if (url.origin === 'https://form.jotform.com') evidence.jotformAttempts.push(item);
|
|
return route.abort();
|
|
});
|
|
for (const { route } of routes) {
|
|
evidence.checks++;
|
|
const page = await context.newPage();
|
|
const response = await page.goto(origin + route, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
assert.equal(response?.status(), 200, `${route} without JavaScript`);
|
|
assert.equal(await page.locator('[data-jotform-form-a] iframe').count(), 1, `${route} without JavaScript`);
|
|
const iframe = page.locator('[data-jotform-form-a] iframe');
|
|
const fallbackMarkup = await iframe.evaluate((element) => element.innerHTML);
|
|
assert.match(fallbackMarkup, /class="jotform-iframe__fallback"/, `${route} without JavaScript`);
|
|
assert.match(fallbackMarkup, /target="_blank" rel="noopener noreferrer"/, `${route} without JavaScript`);
|
|
assert.equal(await page.locator('[data-jotform-form-a] > .jotform-iframe__fallback').count(), 0, `${route} without JavaScript should not show a duplicate parent fallback`);
|
|
if (route.startsWith('/lp/')) {
|
|
const expectedSurface = route === '/lp/early-intervention'
|
|
? '#consultation.lead-form > [data-jotform-form-a].jotform-iframe--embedded'
|
|
: '.form-wrap > [data-jotform-form-a].jotform-iframe--embedded';
|
|
assert.equal(await page.locator(expectedSurface).count(), 1, `${route} without JavaScript`);
|
|
}
|
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), `${route} without JavaScript has horizontal overflow`);
|
|
}
|
|
await context.close();
|
|
}
|
|
|
|
const formAttempts = evidence.jotformAttempts;
|
|
if (expectedIframe) {
|
|
assert.ok(formAttempts.length >= routes.length, 'Expected intercepted Jotform iframe GET attempts');
|
|
for (const attempt of formAttempts) {
|
|
assert.equal(attempt.method, 'GET');
|
|
assert.equal(attempt.path, '/262946078314059');
|
|
assert.match(attempt.search, /^\?formContext=(?:contact|landing)&language=(?:en|es)$/);
|
|
}
|
|
} else {
|
|
assert.equal(formAttempts.length, 0, `${mode} attempted Jotform traffic`);
|
|
}
|
|
assert.equal(evidence.blocked.filter((item) => !['GET', 'HEAD'].includes(item.method)).length, 0, 'A blocked 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, screenshots: evidence.screenshots.map((file) => path.basename(file)), artifactDirectory: output }, null, 2));
|
|
}
|