feat(forms): add local Form A presentation preview

This commit is contained in:
DeCentN2Madness committed 2026-10-25 13:04:00 -07:00
1 parent b102a061fe
commit 7a6cab29db
13 files changed
+983 -8

No files matched your search

+210
View File
@@ -0,0 +1,210 @@
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 expectedMode = process.env.FORM_A_BROWSER_MODE === 'off' ? 'off' : 'mock';
const campaignRoutes = ['/lp/aba-therapy', '/lp/aba-insurance', '/lp/aba-ahcccs', '/lp/aba-private-pay'];
const earlyInterventionRoute = '/lp/early-intervention';
const routes = expectedMode === 'off' ? [...campaignRoutes, earlyInterventionRoute] : ['/contact', '/es/contact', ...campaignRoutes, earlyInterventionRoute];
const output = await fs.mkdtemp('/tmp/aia-form-a-preview-');
const evidence = { mode: expectedMode, routes, widths: [390, 1440], checks: 0, failures: [], blocked: [], productionAttempts: [], 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}`;
const isProduction = (url) => /(?:jotform|api\.azinstitute4autism\.com|google-analytics|googletagmanager|doubleclick)/i.test(url.hostname + url.pathname);
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);
for (const width of evidence.widths) {
const context = await browser.newContext({ viewport: { width, height: width < 500 ? 844 : 900 }, serviceWorkers: 'block' });
await context.routeWebSocket('**/*', (socket) => socket.close());
await context.route('**/*', (route) => {
const request = route.request();
const url = new URL(request.url());
if (url.origin !== origin || !['GET', 'HEAD'].includes(request.method())) {
const item = { origin: url.origin, path: url.pathname, method: request.method() };
evidence.blocked.push(item);
if (isProduction(url)) evidence.productionAttempts.push(item);
return route.abort();
}
return route.continue();
});
const page = await context.newPage();
for (const route of routes) {
evidence.checks++;
try {
const response = await page.goto(origin + route, { waitUntil: 'domcontentloaded', timeout: 15000 });
assert.equal(response?.status(), 200);
if (route === earlyInterventionRoute) {
assert.equal(await page.locator('[data-form-a-preview]').count(), 0);
assert.equal(await page.locator('#consultation[data-forms-mode="off"]').count(), 1);
assert.equal(await page.locator('#consultation button[type="button"][disabled]').count(), 1);
assert.equal(await page.locator('[data-form-status-notice], #consultation-unavailable').count(), 0);
assert.doesNotMatch(await page.locator('.qr-landing').textContent(), /(?:online )?(?:forms?|requests?|submissions?) (?:are |is )?(?:unavailable|not enabled)|detailed medical|diagnostic|insurance-member/i);
assert.ok(await page.locator('a[href="tel:+14806877099"]').count() >= 1);
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
continue;
}
if (expectedMode === 'off') {
assert.equal(await page.locator('[data-form-a-preview]').count(), 0);
assert.equal(await page.locator('#lead-form[data-forms-mode="off"]').count(), 1);
assert.equal(await page.locator('[data-form-status-notice], #campaign-form-status').count(), 0);
assert.equal(await page.locator('#lead-form a[href^="tel:"]').count(), 1);
assert.equal(await page.locator('#lead-form a[href="/privacy-policy"]').count(), 1);
assert.doesNotMatch(await page.locator('#lead-form').textContent(), /(?:online )?(?:forms?|requests?|submissions?) (?:are |is )?(?:unavailable|not enabled)|detailed medical|diagnostic|insurance-member/i);
assert.equal(await page.locator('#lead-form button[type="button"][disabled]').count(), 1);
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
continue;
}
const compact = route.startsWith('/lp/');
const fields = compact ? ['name', 'phone', 'email', 'preferredContact'] : ['name', 'phone', 'email', 'preferredContact', 'message'];
const result = await page.evaluate(({ fields: expected, compact: isCompact }) => ({
enabled: document.querySelector('[data-form-a-preview]')?.dataset.formAPreviewEnabled,
names: [...document.querySelectorAll('[data-form-a-preview] [name]')].map((el) => el.getAttribute('name')),
forms: document.querySelectorAll('form').length,
iframe: document.querySelectorAll('iframe').length,
h1: document.querySelectorAll('main h1').length,
width: document.documentElement.scrollWidth,
expectedCount: expected.length,
isCompact
}), { fields, compact });
assert.equal(result.enabled, 'true');
assert.deepEqual(result.names, fields);
assert.equal(result.forms, 0);
assert.equal(result.iframe, 0);
assert.equal(result.h1, 1);
assert.ok(result.width <= width + 1, `overflow ${result.width}`);
const preview = page.locator('[data-form-a-preview]');
if (route === '/es/contact') {
assert.match(await preview.textContent(), /Su nombre|Método de contacto preferido/);
assert.doesNotMatch(await preview.textContent(), /Your name|Preferred contact method/);
}
const expectedButtonText = route === '/contact' ? 'Submit' : route === '/es/contact' ? 'Enviar' : {
'/lp/aba-therapy': 'Request a Free Consultation',
'/lp/aba-insurance': 'Request Insurance Verification',
'/lp/aba-ahcccs': 'Request a Benefits Check',
'/lp/aba-private-pay': 'Discuss Private-Pay Options'
}[route];
assert.equal(await preview.locator('[data-preview-status]').count(), 1);
assert.equal(await preview.locator('.form-a-preview__caution').count(), compact ? 0 : 1);
if (compact) {
assert.equal(await page.locator('[data-form-status-notice]').count(), 1);
assert.equal((await page.locator('[data-form-status-notice]').textContent()).match(/Preview only — nothing is submitted\./g)?.length, 1);
assert.equal(await page.locator('[data-form-status-notice] a[href="tel:+14806877099"]').count(), 1);
assert.equal(await page.locator('[data-form-status-notice] a[href="/privacy-policy"]').count(), 1);
assert.doesNotMatch(await page.locator('#consultation').textContent(), /(?:online )?(?:forms?|requests?|submissions?) (?:are |is )?(?:unavailable|not enabled)|detailed medical|diagnostic|insurance-member/i);
}
const button = preview.locator('[data-preview-submit]');
assert.equal((await button.textContent()).trim(), expectedButtonText);
const filledStyle = await button.evaluate((element) => {
const style = getComputedStyle(element);
return { background: style.backgroundColor, border: style.borderColor, borderWidth: style.borderWidth, color: style.color };
});
assert.equal(filledStyle.background, 'rgb(255, 189, 89)');
assert.equal(filledStyle.border, 'rgb(255, 189, 89)');
assert.equal(filledStyle.borderWidth, '2px');
assert.equal(filledStyle.color, 'rgb(37, 64, 128)');
await button.hover();
await delay(350);
const hoverStyle = await button.evaluate((element) => {
const style = getComputedStyle(element);
return { background: style.backgroundColor, border: style.borderColor };
});
assert.equal(hoverStyle.background, 'rgba(0, 0, 0, 0)');
assert.equal(hoverStyle.border, 'rgb(255, 145, 77)');
await button.focus();
assert.notEqual(await button.evaluate((element) => getComputedStyle(element).outlineWidth), '0px');
await button.click();
assert.doesNotMatch(await preview.locator('[data-preview-status]').textContent(), /completed locally|terminó localmente/);
await preview.locator('input[name="name"]').fill('Demo Reviewer');
await preview.locator('input[name="phone"]').fill('480-555-0100');
await preview.locator('select[name="preferredContact"]').selectOption('phone');
await button.click();
if (compact) assert.match(await preview.locator('[data-preview-status]').textContent(), /Preview only — nothing is submitted/);
else assert.match(await preview.locator('[data-preview-status]').textContent(), /completed locally|terminó localmente/);
await preview.locator('select[name="preferredContact"]').selectOption('email');
if (route === '/es/contact') {
assert.equal(
(await preview.locator('[id$="-email-requirement"]').textContent())?.trim(),
'(obligatorio para contacto por correo)'
);
}
await button.click();
await assert.match(await preview.locator('[data-preview-status]').textContent(), /email|correo/i);
await preview.locator('input[name="email"]').fill('demo@example.test');
await button.click();
if (compact) assert.match(await preview.locator('[data-preview-status]').textContent(), /Preview only — nothing is submitted/);
else assert.match(await preview.locator('[data-preview-status]').textContent(), /completed locally|terminó localmente/);
if ((route === '/contact' || route === '/lp/aba-therapy') && (width === 390 || width === 1440)) {
const file = `${route === '/contact' ? 'contact' : 'campaign'}-${width}.png`;
await page.screenshot({ path: path.join(output, file), fullPage: true });
evidence.screenshots.push({ route, width, file });
}
} catch (error) {
evidence.failures.push({ route, width, error: String(error.message) });
}
}
await context.close();
}
if (expectedMode === 'mock') {
const noJsContext = await browser.newContext({ viewport: { width: 390, height: 844 }, javaScriptEnabled: false, serviceWorkers: 'block' });
await noJsContext.route('**/*', (route) => {
const request = route.request();
const url = new URL(request.url());
if (url.origin !== origin || !['GET', 'HEAD'].includes(request.method())) {
const item = { origin: url.origin, path: url.pathname, method: request.method(), noJs: true };
evidence.blocked.push(item);
if (isProduction(url)) evidence.productionAttempts.push(item);
return route.abort();
}
return route.continue();
});
for (const route of ['/contact', '/lp/aba-therapy']) {
evidence.checks++;
const page = await noJsContext.newPage();
const response = await page.goto(origin + route, { waitUntil: 'domcontentloaded', timeout: 15000 });
assert.equal(response?.status(), 200);
assert.equal(await page.locator('[data-form-a-preview]').count(), 1);
assert.equal(await page.locator('form').count(), 0);
const bodyText = await page.locator('body').textContent();
if (route === '/contact') assert.match(bodyText, /JavaScript|JavaScript is required|Se requiere JavaScript/);
else assert.match(bodyText, /Preview only — nothing is submitted/);
}
await noJsContext.close();
}
evidence.status = evidence.failures.length || evidence.productionAttempts.length ? 'failed' : 'passed';
} catch (error) {
evidence.error = String(error.message);
} finally {
await browser?.close();
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');
}
evidence.finishedAt = new Date().toISOString();
await fs.writeFile(path.join(output, 'results.json'), JSON.stringify(evidence, null, 2) + '\n');
console.log(JSON.stringify({ ...evidence, output }, null, 2));
if (evidence.status !== 'passed') process.exitCode = 1;
}
+59
View File
@@ -0,0 +1,59 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import test from 'node:test';
import { formAPreviewEnabled } from '../src/utils/form-integration.ts';
const read = (file) => fs.readFileSync(file, 'utf8');
test('Form A preview is explicit and has no live destination in source', () => {
const source = read('src/components/FormAPreview.astro');
assert.match(source, /data-form-a-preview/);
assert.match(source, /type="button"/);
assert.match(source, /submitText/);
assert.match(source, /text\.internalPreview/);
assert.match(source, /dataset\.emailRequired/);
assert.match(source, /border: 2px solid var\(--color-accent/);
assert.doesNotMatch(source, /jotform\.com|api\.azinstitute4autism\.com|fetch\(/i);
assert.equal(formAPreviewEnabled('true', 'false', 'mock'), true);
assert.equal(formAPreviewEnabled('true', 'true', 'mock'), false);
});
test('Form A preview placements are explicit and compact previews omit message cautions', () => {
const contact = read('src/components/pages/ContactPage.astro');
const contactForm = read('src/components/ContactForm.astro');
const campaign = read('src/components/campaign/ConsultationForm.astro');
const preview = read('src/components/FormAPreview.astro');
const messages = read('src/utils/form-messages.ts');
const early = read('src/components/campaign/EarlyInterventionPage.astro');
assert.match(contact, /FormAPreview/);
assert.match(contact, /submitText=\{lang === 'es' \? 'Enviar' : 'Submit'\}/);
assert.match(campaign, /FormAPreview/);
assert.match(campaign, /submitText=\{buttonText\}/);
assert.doesNotMatch(early, /FormAPreview/);
assert.doesNotMatch(preview, /name="inquiryType"/);
assert.match(preview, /variant === 'full'.*form-a-preview__caution/s);
assert.match(contactForm, /textarea[^>]+aria-describedby="contact-sensitive-note"/);
assert.match(contactForm, /text\.formAPreview\.caution/);
assert.match(messages, /Preview only — nothing is submitted/);
assert.match(messages, /Vista previa para reunión interna/);
assert.doesNotMatch(campaign, /detailed medical|diagnostic|insurance-member/i);
assert.doesNotMatch(early, /(?:online )?(?:forms?|requests?|submissions?) (?:are |is )?(?:unavailable|not enabled)|detailed medical|diagnostic|insurance-member/i);
});
test('default environment keeps the preview disabled', () => {
assert.match(read('.env.example'), /PUBLIC_AIA_FORM_A_PREVIEW=false/);
assert.equal(formAPreviewEnabled('false', 'false', 'off'), false);
});
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, /:focus-visible/);
assert.doesNotMatch(css, /https?:\/\/|@import|url\(/i);
assert.match(guide, /formContext/);
assert.match(guide, /multiple native Jotform Submit buttons/i);
assert.match(guide, /honeypot/i);
assert.match(guide, /cross-origin iframe/i);
});
+9
View File
@@ -6,6 +6,7 @@ import {
resolveCounterConfig,
} from '../src/utils/counter-mode.ts';
import {
formAPreviewEnabled,
formModeForRuntime,
formRouteMapping,
logicalFormForRoute,
@@ -36,6 +37,14 @@ test('form configuration defaults off and live remains fail-closed', () => {
assert.equal(resolveMockFormResult('anything-else'), 'success');
});
test('Form A meeting preview requires explicit mock mode and remains staging-only', () => {
assert.equal(formAPreviewEnabled(undefined, 'false', 'mock'), false);
assert.equal(formAPreviewEnabled('true', undefined, 'off'), false);
assert.equal(formAPreviewEnabled('true', 'true', 'mock'), false);
assert.equal(formAPreviewEnabled('true', 'false', 'mock'), true);
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);
assert.equal(formRouteMapping.filter((entry) => entry.kind === 'landing-journey').length, 5);