/** * 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', 'production'].includes(mode)) throw new Error(`Unsupported Form A browser mode: ${mode}`); const expectedIframe = mode === 'isolated-test' || mode === 'production'; const expectedFormMode = mode === 'production' ? 'live' : 'isolated-test'; const expectedEnvironment = mode === 'production' ? 'production' : '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, fallbackChecks: [], 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="${expectedFormMode}"]`).count(), 1, route); assert.equal(await page.locator(`[data-jotform-form-a][data-form-environment="${expectedEnvironment}"]`).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 fallback = page.locator('[data-jotform-form-a] > [data-jotform-iframe-fallback]'); assert.equal(await fallback.count(), 1, `${route} should render one parent fallback`); assert.equal(await fallback.getAttribute('hidden'), '', `${route} fallback should not reserve space before timeout`); assert.equal(await fallback.getAttribute('aria-live'), 'polite', route); assert.equal(await fallback.locator('a').getAttribute('href'), expectedUrl, route); assert.equal(await fallback.locator('a').getAttribute('target'), '_blank', route); assert.equal(await fallback.locator('a').getAttribute('rel'), 'noopener noreferrer', route); assert.equal(await iframe.evaluate((element) => element.innerHTML.trim()), '', `${route} should not keep fallback content inside the iframe`); assert.equal(await page.locator('[data-jotform-form-a] .jotform-iframe__fallback--noscript').count(), 0, `${route} should not render a duplicate noscript fallback while JavaScript is enabled`); 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, locale } 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`); assert.equal(await page.locator(`[data-jotform-form-a][data-form-mode="${expectedFormMode}"]`).count(), 1, `${route} without JavaScript`); const iframe = page.locator('[data-jotform-form-a] iframe'); assert.equal(await iframe.evaluate((element) => element.innerHTML.trim()), '', `${route} without JavaScript should not place fallback content inside the iframe`); const noscriptFallback = page.locator('[data-jotform-form-a] .jotform-iframe__fallback--noscript'); assert.equal(await noscriptFallback.count(), 1, `${route} without JavaScript should expose one localized hosted-form fallback`); assert.equal(await noscriptFallback.isVisible(), true, `${route} without JavaScript fallback should be visible`); assert.equal(await noscriptFallback.locator('a').getAttribute('target'), '_blank', `${route} without JavaScript`); assert.equal(await noscriptFallback.locator('a').getAttribute('rel'), 'noopener noreferrer', `${route} without JavaScript`); assert.match( await noscriptFallback.innerText(), locale === 'es' ? /Si el formulario integrado no se carga,/ : /If the embedded form does not load,/, `${route} without JavaScript should use the localized 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(); } if (expectedIframe) { // Exercise the first-party fallback with a local fixture. Jotform is never // contacted: every frame request is fulfilled or held inside Playwright. let fallbackScenario = 'immediate'; let releaseLateFrame; let timeoutFrameRequested; const context = await browser.newContext({ viewport: { width: 390, height: 844 }, 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(), fixture: true }; evidence.blocked.push(item); if (url.origin === 'https://form.jotform.com' && url.pathname === '/262946078314059') { evidence.jotformAttempts.push(item); if (fallbackScenario === 'delayed') await delay(30); if (fallbackScenario === 'timeout-late') { timeoutFrameRequested?.(); await new Promise((resolve) => { releaseLateFrame = resolve; }); } return route.fulfill({ contentType: 'text/html', body: 'Local Form A Fixture

Fixture only

' }); } return route.abort(); }); const openFallbackScenario = async (name) => { const page = await context.newPage(); // Keep the production 15-second timeout literal while making this local // fixture deterministic and fast. The override is scoped to the page. await page.addInitScript(() => { const originalSetTimeout = window.setTimeout.bind(window); window.setTimeout = (callback, timeout, ...args) => originalSetTimeout(callback, timeout === 15_000 ? 80 : timeout, ...args); }); const response = await page.goto(origin + '/contact', { waitUntil: 'domcontentloaded', timeout: 15000 }); assert.equal(response?.status(), 200, `fallback ${name} outer page`); return { page, fallback: page.locator('[data-jotform-iframe-fallback]') }; }; fallbackScenario = 'immediate'; { const { page, fallback } = await openFallbackScenario('immediate'); await page.waitForTimeout(30); assert.equal(await fallback.isVisible(), false, 'A successful iframe load keeps fallback hidden'); evidence.fallbackChecks.push('successful-load-hidden'); await page.close(); } fallbackScenario = 'delayed'; { const { page, fallback } = await openFallbackScenario('delayed'); await page.waitForTimeout(45); assert.equal(await fallback.isVisible(), false, 'A delayed iframe load stays hidden before the 15-second timeout'); evidence.fallbackChecks.push('delayed-load-hidden-before-timeout'); await page.close(); } fallbackScenario = 'timeout-late'; { const requested = new Promise((resolve) => { timeoutFrameRequested = resolve; }); const { page, fallback } = await openFallbackScenario('timeout-late'); await requested; await page.waitForTimeout(100); assert.equal(await fallback.isVisible(), true, 'A missing iframe load reveals the accessible hosted-form fallback after timeout'); assert.equal(await fallback.getAttribute('role'), 'status'); releaseLateFrame?.(); await page.waitForFunction(() => document.querySelector('[data-jotform-iframe-fallback]')?.hidden === true); assert.equal(await fallback.isVisible(), false, 'A late iframe load hides the timeout fallback consistently'); evidence.fallbackChecks.push('timeout-visible', 'late-load-hidden'); await page.close(); } 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)); }