148 lines
7.3 KiB
JavaScript
148 lines
7.3 KiB
JavaScript
/**
|
|
* Read-only Form A iframe sizing acceptance probe.
|
|
*
|
|
* This intentionally requires a separate affirmative environment value
|
|
* because it permits GET/HEAD requests to form.jotform.com. It never fills,
|
|
* focuses, clicks, or submits an iframe control. All other external origins,
|
|
* non-read methods, downloads, and WebSockets are blocked and recorded.
|
|
*
|
|
* Build the isolated-test artifact first, then run:
|
|
* FORM_A_IFRAME_REAL_LOAD_APPROVED=true \
|
|
* FORM_A_IFRAME_JOTFORM_ASSETS_APPROVED=true \
|
|
* PLAYWRIGHT_MODULE=/path/to/playwright/index.mjs \
|
|
* CHROMIUM_EXECUTABLE=/path/to/chromium \
|
|
* node tools/jotform-form-a-real-iframe-sizing.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';
|
|
|
|
if (process.env.FORM_A_IFRAME_REAL_LOAD_APPROVED !== 'true') {
|
|
throw new Error('Set FORM_A_IFRAME_REAL_LOAD_APPROVED=true to permit read-only requests to form.jotform.com.');
|
|
}
|
|
|
|
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
const playwrightModule = process.env.PLAYWRIGHT_MODULE
|
|
? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href
|
|
: 'playwright';
|
|
const jotformOrigin = 'https://form.jotform.com';
|
|
const jotformAssetOrigin = 'https://cdn.jotfor.ms';
|
|
const allowJotformAssets = process.env.FORM_A_IFRAME_JOTFORM_ASSETS_APPROVED === 'true';
|
|
const allowedOrigins = new Set([jotformOrigin, ...(allowJotformAssets ? [jotformAssetOrigin] : [])]);
|
|
const routes = [
|
|
{ route: '/contact', context: 'contact', locale: 'en', file: 'contact' },
|
|
{ route: '/es/contact', context: 'contact', locale: 'es', file: 'es-contact' },
|
|
{ route: '/lp/aba-therapy', context: 'landing', locale: 'en', file: 'aba-therapy' },
|
|
{ route: '/lp/aba-insurance', context: 'landing', locale: 'en', file: 'aba-insurance' },
|
|
{ route: '/lp/aba-ahcccs', context: 'landing', locale: 'en', file: 'aba-ahcccs' },
|
|
{ route: '/lp/aba-private-pay', context: 'landing', locale: 'en', file: 'aba-private-pay' },
|
|
{ route: '/lp/early-intervention', context: 'landing', locale: 'en', file: 'early-intervention' },
|
|
];
|
|
const widths = [320, 390, 768, 1440];
|
|
const output = await fs.mkdtemp('/tmp/aia-form-a-real-iframe-sizing-');
|
|
const evidence = {
|
|
routes: routes.map(({ route, context, locale }) => ({ route, context, locale })),
|
|
widths,
|
|
allowedOrigins: [...allowedOrigins],
|
|
allowedJotformRequests: [],
|
|
blockedExternalRequests: [],
|
|
forbiddenAttempts: [],
|
|
screenshots: [],
|
|
status: 'failed',
|
|
};
|
|
const forbiddenHost = /(?:api\.azinstitute4autism\.com|google-analytics|googletagmanager|googleadservices|doubleclick)/i;
|
|
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(playwrightModule);
|
|
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 attempts = 0; attempts < 60 && !(await reachable()); attempts++) 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 < 641 ? 844 : 960 },
|
|
serviceWorkers: 'block',
|
|
acceptDownloads: false,
|
|
});
|
|
await context.routeWebSocket('**/*', (socket) => socket.close());
|
|
await context.route('**/*', async (route) => {
|
|
const request = route.request();
|
|
const url = new URL(request.url());
|
|
const item = { origin: url.origin, path: url.pathname, method: request.method() };
|
|
if (url.origin === origin && ['GET', 'HEAD'].includes(request.method())) return route.continue();
|
|
if (allowedOrigins.has(url.origin) && ['GET', 'HEAD'].includes(request.method())) {
|
|
evidence.allowedJotformRequests.push(item);
|
|
return route.continue();
|
|
}
|
|
evidence.blockedExternalRequests.push(item);
|
|
if (forbiddenHost.test(url.hostname)) evidence.forbiddenAttempts.push(item);
|
|
return route.abort();
|
|
});
|
|
const page = await context.newPage();
|
|
for (const expected of routes) {
|
|
const response = await page.goto(origin + expected.route, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
assert.equal(response?.status(), 200, expected.route);
|
|
const iframe = page.locator('[data-jotform-form-a] iframe');
|
|
const expectedUrl = `${jotformOrigin}/262946078314059?formContext=${expected.context}&language=${expected.locale}`;
|
|
assert.equal(await iframe.count(), 1, expected.route);
|
|
assert.equal(await iframe.getAttribute('src'), expectedUrl, expected.route);
|
|
assert.equal(await iframe.getAttribute('sandbox'), 'allow-forms allow-scripts allow-same-origin', expected.route);
|
|
assert.equal(await iframe.getAttribute('referrerpolicy'), 'no-referrer', expected.route);
|
|
const box = await iframe.boundingBox();
|
|
const expectedHeight = expected.context === 'contact'
|
|
? width < 641 ? 1088 : 960
|
|
: width < 641 ? 800 : 672;
|
|
assert.ok(box && box.width > 0 && box.height >= expectedHeight, `${expected.route}: context-specific frame height is insufficient`);
|
|
await page.waitForTimeout(1200);
|
|
const screenshot = path.join(output, `${expected.file}-${width}.png`);
|
|
await page.screenshot({ path: screenshot, fullPage: true });
|
|
evidence.screenshots.push(screenshot);
|
|
}
|
|
await context.close();
|
|
}
|
|
assert.ok(evidence.allowedJotformRequests.length >= routes.length, 'Expected read-only Jotform iframe requests.');
|
|
assert.equal(evidence.forbiddenAttempts.length, 0, `Forbidden external requests: ${JSON.stringify(evidence.forbiddenAttempts)}`);
|
|
assert.equal(evidence.blockedExternalRequests.filter((item) => !['GET', 'HEAD'].includes(item.method)).length, 0, 'A non-read external 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?.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));
|
|
}
|