feat(forms): add local Form A presentation preview
This commit is contained in:
13 files changed
+983
-8
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user