feat(forms): add isolated Form A consultation testing
This commit is contained in:
25 files changed
+1046
-253
No files matched your search
@@ -49,7 +49,8 @@ test('Jotform handoff preserves AIA form tokens and accessible native controls',
|
||||
const css = read('integrations/jotform/aia-form-a.css');
|
||||
const guide = read('integrations/jotform/README.md');
|
||||
for (const value of ['#ffbd59', '#ff914d', '#254080', '#fcf9f5', '#edeef3']) assert.match(css, new RegExp(value));
|
||||
assert.match(css, /#form \.form-submit-button/);
|
||||
assert.match(css, /\.form-all \.form-submit-button/);
|
||||
assert.match(css, /\.form-all button\[type="submit"\]/);
|
||||
assert.match(css, /:focus-visible/);
|
||||
assert.doesNotMatch(css, /https?:\/\/|@import|url\(/i);
|
||||
assert.match(guide, /formContext/);
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 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.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-open]');
|
||||
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-open')), '', 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-open]').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));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
buildFormAIframeUrl,
|
||||
formAConsultationTestPlacements,
|
||||
formAIframeId,
|
||||
formAIframeOrigin,
|
||||
formAIframePlacements,
|
||||
@@ -15,6 +16,7 @@ const isolated = (overrides = {}) => ({
|
||||
rawMode: 'isolated-test',
|
||||
liveApproved: 'false',
|
||||
isolatedTestApproved: 'true',
|
||||
consultationTestApproved: 'false',
|
||||
productionApproved: 'false',
|
||||
environmentRole: 'isolated-test',
|
||||
allowIndexing: 'false',
|
||||
@@ -23,10 +25,18 @@ const isolated = (overrides = {}) => ({
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const consultation = (overrides = {}) => isolated({
|
||||
consultationTestApproved: 'true',
|
||||
route: '/client-consultation',
|
||||
locale: 'en',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const production = (overrides = {}) => ({
|
||||
rawMode: 'live',
|
||||
liveApproved: 'true',
|
||||
isolatedTestApproved: 'false',
|
||||
consultationTestApproved: 'true',
|
||||
productionApproved: 'true',
|
||||
environmentRole: 'production',
|
||||
allowIndexing: 'true',
|
||||
@@ -50,6 +60,10 @@ test('Form A iframe uses only fixed, approved route parameters', () => {
|
||||
buildFormAIframeUrl('en', 'landing'),
|
||||
'https://form.jotform.com/262946078314059?formContext=landing&language=en',
|
||||
);
|
||||
assert.equal(
|
||||
buildFormAIframeUrl('es', 'consultation'),
|
||||
'https://form.jotform.com/262946078314059?formContext=consultation&language=es',
|
||||
);
|
||||
const english = resolveFormAIframe(isolated());
|
||||
const spanish = resolveFormAIframe(isolated({ route: '/es/contact', locale: 'es' }));
|
||||
assert.equal(english.fallbackText, 'If the embedded form does not load,');
|
||||
@@ -65,6 +79,17 @@ test('Form A iframe uses only fixed, approved route parameters', () => {
|
||||
'/lp/aba-private-pay',
|
||||
'/lp/early-intervention',
|
||||
]);
|
||||
assert.deepEqual(formAConsultationTestPlacements.map((placement) => placement.route), [
|
||||
'/client-consultation',
|
||||
'/es/client-consultation',
|
||||
'/schedule-consultation',
|
||||
'/aba-therapy',
|
||||
'/autism-evaluations',
|
||||
'/learner-social-club',
|
||||
'/es/aba-therapy',
|
||||
'/es/autism-evaluations',
|
||||
'/es/learner-social-club',
|
||||
]);
|
||||
});
|
||||
|
||||
test('Form A iframe requires every isolated-test or production approval gate', () => {
|
||||
@@ -103,9 +128,44 @@ test('Form A iframe requires every isolated-test or production approval gate', (
|
||||
assert.equal(formModeForRuntime(resolveFormMode('live', 'true')), 'off');
|
||||
});
|
||||
|
||||
test('Only the seven allowlisted placements can render the adapter, with no helper, tracking, or parent submission code', () => {
|
||||
test('consultation is isolated-test-only and requires its additional explicit approval', () => {
|
||||
const enabled = resolveFormAIframe(consultation());
|
||||
assert.equal(enabled.enabled, true);
|
||||
assert.equal(enabled.context, 'consultation');
|
||||
assert.equal(enabled.title, 'AIA consultation form');
|
||||
assert.equal(enabled.url, 'https://form.jotform.com/262946078314059?formContext=consultation&language=en');
|
||||
|
||||
const spanish = resolveFormAIframe(consultation({ route: '/es/client-consultation', locale: 'es' }));
|
||||
assert.equal(spanish.enabled, true);
|
||||
assert.equal(spanish.title, 'Formulario de consulta de AIA');
|
||||
assert.equal(spanish.url, 'https://form.jotform.com/262946078314059?formContext=consultation&language=es');
|
||||
|
||||
for (const placement of formAConsultationTestPlacements) {
|
||||
const config = resolveFormAIframe(consultation({ route: placement.route, locale: placement.locale }));
|
||||
assert.equal(config.enabled, true, placement.route);
|
||||
assert.equal(config.context, 'consultation', placement.route);
|
||||
assert.equal(config.url, buildFormAIframeUrl(placement.locale, 'consultation'), placement.route);
|
||||
}
|
||||
|
||||
assert.equal(resolveFormAIframe(isolated({ route: '/client-consultation' })).reason, 'consultation-test-not-approved');
|
||||
assert.equal(resolveFormAIframe(consultation({ consultationTestApproved: undefined })).reason, 'consultation-test-not-approved');
|
||||
assert.equal(resolveFormAIframe(consultation({ consultationTestApproved: 'yes' })).reason, 'consultation-test-not-approved');
|
||||
assert.equal(resolveFormAIframe(consultation({ rawMode: 'mock' })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(consultation({ liveApproved: 'true' })).reason, 'live-approval-not-disabled');
|
||||
assert.equal(resolveFormAIframe(consultation({ isolatedTestApproved: 'false' })).reason, 'isolated-test-not-approved');
|
||||
assert.equal(resolveFormAIframe(consultation({ environmentRole: 'staging' })).reason, 'invalid-environment-role');
|
||||
assert.equal(resolveFormAIframe(consultation({ allowIndexing: 'true' })).reason, 'indexing-enabled');
|
||||
assert.equal(resolveFormAIframe(consultation({ route: '/es/aba-therapy', locale: 'en' })).reason, 'locale-not-authorized');
|
||||
assert.equal(resolveFormAIframe(production({ route: '/client-consultation' })).reason, 'route-not-authorized');
|
||||
assert.equal(resolveFormAIframe(production({ route: '/client-consultation', consultationTestApproved: 'true' })).reason, 'route-not-authorized');
|
||||
});
|
||||
|
||||
test('the adapter preserves seven production routes and supports the separate bilingual consultation test surface', () => {
|
||||
const contact = fs.readFileSync('src/components/pages/ContactPage.astro', 'utf8');
|
||||
const adapter = fs.readFileSync('src/components/JotformIframe.astro', 'utf8');
|
||||
const shell = fs.readFileSync('src/components/FormShell.astro', 'utf8');
|
||||
const drawer = fs.readFileSync('src/components/ServiceInquiryDrawer.astro', 'utf8');
|
||||
const layout = fs.readFileSync('src/layouts/PageLayout.astro', 'utf8');
|
||||
const campaign = fs.readFileSync('src/components/campaign/ConsultationForm.astro', 'utf8');
|
||||
const earlyIntervention = fs.readFileSync('src/components/campaign/EarlyInterventionPage.astro', 'utf8');
|
||||
|
||||
@@ -113,6 +173,7 @@ test('Only the seven allowlisted placements can render the adapter, with no help
|
||||
assert.match(contact, /route = lang === 'es' \? '\/es\/contact' : '\/contact'/);
|
||||
assert.match(contact, /JotformIframe/);
|
||||
assert.match(contact, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
|
||||
assert.match(contact, /PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED/);
|
||||
assert.match(campaign, /resolveFormAIframe/);
|
||||
assert.match(campaign, /JotformIframe/);
|
||||
assert.match(campaign, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
|
||||
@@ -125,16 +186,32 @@ test('Only the seven allowlisted placements can render the adapter, with no help
|
||||
assert.match(earlyIntervention, /route: '\/lp\/early-intervention'/);
|
||||
assert.match(earlyIntervention, /JotformIframe/);
|
||||
assert.match(earlyIntervention, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
|
||||
assert.match(earlyIntervention, /PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED/);
|
||||
assert.match(campaign, /PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED/);
|
||||
assert.match(shell, /resolveFormAIframe/);
|
||||
assert.match(shell, /PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED/);
|
||||
assert.match(shell, /JotformIframe/);
|
||||
assert.match(drawer, /lang: SupportedLocale/);
|
||||
assert.match(drawer, /data-service-inquiry-open/);
|
||||
assert.match(drawer, /data-explicit-activation/);
|
||||
assert.match(drawer, /deferUntilActivated/);
|
||||
assert.match(drawer, /aia:jotform-activate/);
|
||||
assert.match(drawer, /Cerrar el formulario de consulta/);
|
||||
assert.doesNotMatch(drawer, /No WAITLIST!|guarantee your child|now open for enrollment/i);
|
||||
assert.match(layout, /entry\.data\.lang === 'en' \|\| entry\.data\.lang === 'es'/);
|
||||
assert.match(adapter, /referrerpolicy="no-referrer"/);
|
||||
assert.match(adapter, /sandbox="allow-forms allow-scripts allow-same-origin"/);
|
||||
assert.match(adapter, /data-form-mode=\{config\.mode\}/);
|
||||
assert.match(adapter, /data-form-environment=\{config\.environmentRole\}/);
|
||||
assert.match(adapter, /data-jotform-iframe-frame/);
|
||||
assert.match(adapter, /data-jotform-iframe-url/);
|
||||
assert.match(adapter, /data-jotform-deferred/);
|
||||
assert.match(adapter, /data-jotform-iframe-fallback hidden aria-live="polite" role="status"/);
|
||||
assert.match(adapter, /<noscript>/);
|
||||
assert.match(adapter, /fallbackTimeoutMs = 15_000/);
|
||||
assert.match(adapter, /frame\.addEventListener\('load', onLoad, \{ once: true \}\)/);
|
||||
assert.match(adapter, /document\.addEventListener\('astro:before-swap', dispose, \{ once: true \}\)/);
|
||||
assert.match(adapter, /container\.addEventListener\('aia:jotform-activate'/);
|
||||
assert.doesNotMatch(adapter, /set:html=|fallbackMarkup|escapeHtml\(/);
|
||||
assert.match(adapter, /target="_blank" rel="noopener noreferrer"/);
|
||||
assert.match(adapter, /--jotform-frame-height: 60rem/);
|
||||
@@ -143,6 +220,8 @@ test('Only the seven allowlisted placements can render the adapter, with no help
|
||||
assert.match(adapter, /\.jotform-iframe--embedded \{\s*--jotform-frame-height: 42rem/);
|
||||
assert.match(adapter, /\.jotform-iframe__frame \{\s*height: 68rem/);
|
||||
assert.match(adapter, /\.jotform-iframe--embedded \.jotform-iframe__frame \{\s*height: 50rem/);
|
||||
assert.match(adapter, /\.jotform-iframe--drawer \{\s*--jotform-frame-height: 72rem/);
|
||||
assert.match(adapter, /\.jotform-iframe--drawer \.jotform-iframe__frame \{\s*height: 82rem/);
|
||||
assert.doesNotMatch(adapter, /for-form-embed-handler|jotformEmbedHandler|onload=|postMessage|fetch\(|form\.submit|analytics|googletag/i);
|
||||
assert.doesNotMatch(campaign, /262946078314059/);
|
||||
});
|
||||
|
||||
@@ -46,12 +46,34 @@ const formAProductionUrls = new Map([
|
||||
['/lp/early-intervention', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
]);
|
||||
|
||||
const formAConsultationStaticUrls = new Map([
|
||||
['/client-consultation', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
['/es/client-consultation', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
['/schedule-consultation', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
]);
|
||||
|
||||
const formAConsultationDrawerUrls = new Map([
|
||||
['/aba-therapy', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
['/autism-evaluations', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
['/learner-social-club', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
['/es/aba-therapy', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
['/es/autism-evaluations', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
['/es/learner-social-club', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
]);
|
||||
|
||||
const formAIsolatedStaticUrls = new Map([...formAProductionUrls, ...formAConsultationStaticUrls]);
|
||||
|
||||
function iframeSources(html) {
|
||||
// Astro correctly escapes query separators in serialized HTML attributes.
|
||||
// Normalize that representation before comparing the fixed allowlisted URL.
|
||||
return [...html.matchAll(/<iframe\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi)].map((match) => match[1].replaceAll('&', '&'));
|
||||
}
|
||||
|
||||
function deferredIframeSources(html) {
|
||||
return [...html.matchAll(/<iframe\b[^>]*\bdata-jotform-iframe-url=["']([^"']+)["'][^>]*>/gi)]
|
||||
.map((match) => match[1].replaceAll('&', '&'));
|
||||
}
|
||||
|
||||
function formAAdapters(html) {
|
||||
return [...html.matchAll(/<section\b[^>]*\bdata-jotform-form-a\b[^>]*>/gi)].map((match) => match[0]);
|
||||
}
|
||||
@@ -71,6 +93,8 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
const formAEnabled = mode === 'isolated-test' || mode === 'production';
|
||||
const production = mode === 'production';
|
||||
const stagingPolicy = mode === 'staging' || mode === 'isolated-test';
|
||||
const staticFormAUrls = production ? formAProductionUrls : mode === 'isolated-test' ? formAIsolatedStaticUrls : new Map();
|
||||
const deferredFormAUrls = mode === 'isolated-test' ? formAConsultationDrawerUrls : new Map();
|
||||
const htmlFiles = (await fg('**/*.html', { cwd: root, absolute: true })).sort();
|
||||
const routes = [];
|
||||
const canonicalMap = new Map();
|
||||
@@ -80,6 +104,7 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
let activeForms = 0;
|
||||
let secretSignals = [];
|
||||
let formAIframes = 0;
|
||||
let deferredFormAIframes = 0;
|
||||
|
||||
for (const file of htmlFiles) {
|
||||
const html = await readText(file);
|
||||
@@ -90,7 +115,8 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
const actions = formActions(html);
|
||||
const nonSearchActions = actions.filter((action) => action && action !== '/search');
|
||||
const trailingSlashUrls = rootRelativeUrls(html).filter(hasTerminalSlash);
|
||||
const expectedFormAUrl = formAEnabled ? formAProductionUrls.get(route) : undefined;
|
||||
const expectedFormAUrl = formAEnabled ? staticFormAUrls.get(route) : undefined;
|
||||
const expectedDeferredFormAUrl = formAEnabled ? deferredFormAUrls.get(route) : undefined;
|
||||
const signals = unsafeSignals(html, formAEnabled);
|
||||
const jotformFrames = iframeSources(html).filter((src) => {
|
||||
try {
|
||||
@@ -99,6 +125,13 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const deferredJotformFrames = deferredIframeSources(html).filter((src) => {
|
||||
try {
|
||||
return new URL(src).origin === 'https://form.jotform.com';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const adapters = formAAdapters(html);
|
||||
if (!title) errors.push(`${route}: missing title`);
|
||||
if (!canonical) errors.push(`${route}: missing canonical`);
|
||||
@@ -122,8 +155,16 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
errors.push(`${route}: Form A adapter lacks the approved ${expectedMode} ${expectedEnvironment} gate`);
|
||||
}
|
||||
formAIframes += jotformFrames.length;
|
||||
} else if (jotformFrames.length || adapters.length || /262946078314059/i.test(html)) {
|
||||
errors.push(`${route}: unauthorized Form A iframe or identifier in production artifact`);
|
||||
} else if (expectedDeferredFormAUrl) {
|
||||
if (jotformFrames.length || deferredJotformFrames.length !== 1 || deferredJotformFrames[0] !== expectedDeferredFormAUrl) {
|
||||
errors.push(`${route}: missing or invalid deferred consultation Form A iframe URL`);
|
||||
}
|
||||
if (adapters.length !== 1 || !adapters[0].includes('data-form-mode="isolated-test"') || !adapters[0].includes('data-form-environment="isolated-test"') || !adapters[0].includes('data-jotform-deferred="true"')) {
|
||||
errors.push(`${route}: deferred consultation adapter lacks the approved isolated-test gate`);
|
||||
}
|
||||
deferredFormAIframes += deferredJotformFrames.length;
|
||||
} else if (jotformFrames.length || deferredJotformFrames.length || adapters.length || /262946078314059/i.test(html)) {
|
||||
errors.push(`${route}: unauthorized Form A iframe or identifier in ${mode} artifact`);
|
||||
}
|
||||
}
|
||||
if (nonSearchActions.length) activeForms += nonSearchActions.length;
|
||||
@@ -160,10 +201,11 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
digest.update('\n');
|
||||
}
|
||||
if (formAEnabled) {
|
||||
const expectedFormAFiles = new Set([...formAProductionUrls.keys()].map((route) => route === '/' ? 'index.html' : `${route.slice(1)}/index.html`));
|
||||
const expectedFormAFiles = new Set([...staticFormAUrls.keys(), ...deferredFormAUrls.keys()].map((route) => route === '/' ? 'index.html' : `${route.slice(1)}/index.html`));
|
||||
const unexpectedFormAFiles = formAResourceFiles.filter((file) => !expectedFormAFiles.has(file));
|
||||
if (unexpectedFormAFiles.length) errors.push(`Form A identifier appears outside approved production routes: ${unexpectedFormAFiles.join(', ')}`);
|
||||
if (formAIframes !== formAProductionUrls.size) errors.push(`production artifact has ${formAIframes} approved Form A iframe(s), expected ${formAProductionUrls.size}`);
|
||||
if (formAIframes !== staticFormAUrls.size) errors.push(`${mode} artifact has ${formAIframes} approved static Form A iframe(s), expected ${staticFormAUrls.size}`);
|
||||
if (deferredFormAIframes !== deferredFormAUrls.size) errors.push(`${mode} artifact has ${deferredFormAIframes} approved deferred Form A iframe(s), expected ${deferredFormAUrls.size}`);
|
||||
} else if (formAResourceFiles.length) {
|
||||
errors.push(`isolated-test Form A identifier appears in artifact: ${formAResourceFiles.join(', ')}`);
|
||||
}
|
||||
@@ -175,6 +217,7 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
reviewIndex,
|
||||
activeNativeFormActions: activeForms,
|
||||
formAIframes,
|
||||
deferredFormAIframes,
|
||||
artifactFiles: files.length,
|
||||
artifactSha256: digest.digest('hex'),
|
||||
routes,
|
||||
|
||||
@@ -16,11 +16,26 @@ async function fixture(mode = 'staging') {
|
||||
['lp/aba-private-pay/index.html', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
['lp/early-intervention/index.html', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
];
|
||||
const consultationFrames = [
|
||||
['client-consultation/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
['es/client-consultation/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
['schedule-consultation/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
];
|
||||
const consultationDrawers = [
|
||||
['aba-therapy/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
['autism-evaluations/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
['learner-social-club/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
['es/aba-therapy/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
['es/autism-evaluations/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
['es/learner-social-club/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
];
|
||||
const pages = mode === 'staging'
|
||||
? [['index.html', 'noindex,nofollow', ''] , ['lp/index.html', 'noindex,nofollow', '']]
|
||||
: mode === 'isolated-test'
|
||||
? [
|
||||
...productionFrames.map(([file, frame]) => [file, 'noindex,nofollow', `<section data-jotform-form-a data-form-mode="isolated-test" data-form-environment="isolated-test"><iframe src="${frame}"></iframe></section>`]),
|
||||
...consultationFrames.map(([file, frame]) => [file, 'noindex,nofollow', `<section data-jotform-form-a data-form-mode="isolated-test" data-form-environment="isolated-test"><iframe src="${frame}"></iframe></section>`]),
|
||||
...consultationDrawers.map(([file, frame]) => [file, 'noindex,nofollow', `<section data-jotform-form-a data-form-mode="isolated-test" data-form-environment="isolated-test" data-jotform-deferred="true"><iframe data-jotform-iframe-url="${frame}"></iframe></section>`]),
|
||||
['lp/index.html', 'noindex,nofollow', ''],
|
||||
]
|
||||
: mode === 'production'
|
||||
@@ -49,10 +64,11 @@ test('indexing artifact requires sitemap and excludes review index', async () =>
|
||||
assert.equal(result.reviewIndex, false);
|
||||
});
|
||||
|
||||
test('isolated-test artifact permits exactly the seven approved Form A iframe placements under staging safeguards', async () => {
|
||||
test('isolated-test artifact permits ten static and six user-activated consultation Form A placements under staging safeguards', async () => {
|
||||
const result = await verifyArtifact(await fixture('isolated-test'), 'isolated-test');
|
||||
assert.equal(result.passed, true);
|
||||
assert.equal(result.formAIframes, 7);
|
||||
assert.equal(result.formAIframes, 10);
|
||||
assert.equal(result.deferredFormAIframes, 6);
|
||||
assert.equal(result.sitemapUrls, 0);
|
||||
assert.equal(result.reviewIndex, true);
|
||||
});
|
||||
|
||||
@@ -45,12 +45,15 @@ test('Form A meeting preview requires explicit mock mode and remains staging-onl
|
||||
assert.equal(formAPreviewEnabled('TRUE', 'FALSE', 'mock'), true);
|
||||
});
|
||||
|
||||
test('form mapping covers nine native instances and five landing journeys', () => {
|
||||
assert.equal(formRouteMapping.filter((entry) => entry.kind === 'native-instance').length, 9);
|
||||
test('form mapping covers twelve native instances and five landing journeys', () => {
|
||||
assert.equal(formRouteMapping.filter((entry) => entry.kind === 'native-instance').length, 12);
|
||||
assert.equal(formRouteMapping.filter((entry) => entry.kind === 'landing-journey').length, 5);
|
||||
assert.equal(logicalFormForRoute('/contact'), 'general');
|
||||
assert.equal(logicalFormForRoute('/es/client-consultation'), 'service');
|
||||
assert.equal(logicalFormForRoute('/referrals'), 'referral');
|
||||
for (const route of ['/es/aba-therapy', '/es/autism-evaluations', '/es/learner-social-club']) {
|
||||
assert.equal(logicalFormForRoute(route), 'service');
|
||||
}
|
||||
for (const route of ['/lp/aba-therapy', '/lp/aba-insurance', '/lp/aba-ahcccs', '/lp/aba-private-pay']) {
|
||||
assert.equal(logicalFormForRoute(route), 'general');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user