refactor(forms): enable approved Form A placements by default
This commit is contained in:
41 files changed
+862
-523
No files matched your search
+31
-16
@@ -81,20 +81,31 @@ test('photos, responsive variants and licensed fonts are self-hosted', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('inactive lead presentation has no native submission surface or live integration runtime', () => {
|
||||
test('campaign panels use bounded default live frames or explicitly inactive controls', () => {
|
||||
for (const page of pages) {
|
||||
const $ = html(page);
|
||||
assert.equal($('form').length, 0);
|
||||
assert.equal($('#lead-form').attr('data-forms-mode'), 'off');
|
||||
assert.equal($('#lead-form [name]:not([disabled])').length, 0);
|
||||
assert.equal($('#lead-form button').attr('type'), 'button');
|
||||
assert.ok($('#lead-form button').is('[disabled]'));
|
||||
const live = $('[data-jotform-form-a]').length === 1;
|
||||
if (!live) {
|
||||
assert.equal($('#lead-form').attr('data-forms-mode'), 'off');
|
||||
assert.equal($('#lead-form [name]:not([disabled])').length, 0);
|
||||
assert.equal($('#lead-form button').attr('type'), 'button');
|
||||
assert.ok($('#lead-form button').is('[disabled]'));
|
||||
} else {
|
||||
assert.equal($('[data-jotform-form-a]').attr('data-form-mode'), 'live');
|
||||
assert.equal($('iframe').attr('src'), 'https://form.jotform.com/262946078314059?formContext=landing&language=en');
|
||||
assert.equal($('#lead-form').length, 0);
|
||||
}
|
||||
assert.equal($('[data-form-status-notice], #campaign-form-status').length, 0);
|
||||
assert.equal($('#lead-form a[href^="tel:"]').length, 1);
|
||||
assert.equal($('#lead-form a[href="/privacy-policy"]').length, 1);
|
||||
assert.ok($('.consultation a[href^="tel:"]').length);
|
||||
assert.ok($('a[href="/privacy-policy"]').length);
|
||||
if (!live) {
|
||||
assert.equal($('#lead-form a[href^="tel:"]').length, 1);
|
||||
assert.equal($('#lead-form a[href="/privacy-policy"]').length, 1);
|
||||
}
|
||||
assert.doesNotMatch($('#lead-form').text(), /(?:online )?(?:forms?|requests?|submissions?) (?:are |is )?(?:unavailable|not enabled)|detailed medical|diagnostic|insurance-member/i);
|
||||
assert.equal($('#lead-form p:empty').length, 0);
|
||||
assert.equal($('iframe').length, 0);
|
||||
assert.equal($('iframe').length, live ? 1 : 0);
|
||||
assert.equal($('script[src]').length, 0);
|
||||
assert.doesNotMatch($('script').text(), /gtag|googletagmanager|sessionStorage|fetch\(|utm_|gclid|postMessage/);
|
||||
}
|
||||
@@ -108,8 +119,8 @@ test('campaign contact paths remain without unavailable-form messaging', () => {
|
||||
assert.doesNotMatch(heroNote, /online forms are not enabled/i);
|
||||
assert.ok($('.hero .form-note a[href^="tel:"]').length);
|
||||
assert.doesNotMatch($('#lead-form').text(), /By submitting this form/i);
|
||||
assert.ok($('#lead-form a[href^="tel:"]').length);
|
||||
assert.ok($('#lead-form a[href="/privacy-policy"]').length);
|
||||
assert.ok($('a[href^="tel:"]').length);
|
||||
assert.ok($('a[href="/privacy-policy"]').length);
|
||||
assert.doesNotMatch($('#lead-form').text(), /(?:online )?(?:forms?|requests?|submissions?) (?:are |is )?(?:unavailable|not enabled)/i);
|
||||
assert.doesNotMatch($('.consultation__contact').text(), /Online (?:forms|requests|submissions)/i);
|
||||
}
|
||||
@@ -146,16 +157,20 @@ test('early-intervention preserves its own publication identity and source secti
|
||||
assert.equal($('h1').length, 1);
|
||||
assert.equal($('main').length, 1);
|
||||
assert.equal($('link[rel="canonical"]').attr('href'), 'https://www.azinstitute4autism.com/lp/early-intervention');
|
||||
assert.equal($('.qr-landing section').length, 7);
|
||||
assert.equal($('.qr-landing section:not([data-jotform-form-a])').length, 7);
|
||||
assert.equal($('.qr-benefit-card').length, 3);
|
||||
assert.equal($('.qr-process-step').length, 3);
|
||||
assert.equal($('form, input[type="hidden"], script[src]').length, 0);
|
||||
assert.equal($('#consultation-heading').text(), 'Contact AIA about early intervention');
|
||||
assert.equal($('#consultation input').length, 3);
|
||||
assert.equal($('#consultation input:disabled').length, 3);
|
||||
assert.equal($('#consultation').attr('data-forms-mode'), 'off');
|
||||
assert.equal($('#consultation [data-jotform-form-a]').length, 0);
|
||||
assert.equal($('#consultation button').attr('type'), 'button');
|
||||
const live = $('#consultation [data-jotform-form-a]').length === 1;
|
||||
assert.equal($('#consultation').attr('data-forms-mode'), live ? 'live' : 'off');
|
||||
if (live) {
|
||||
assert.equal($('#consultation input').length, 0);
|
||||
assert.equal($('#consultation iframe').attr('src'), 'https://form.jotform.com/262946078314059?formContext=landing&language=en');
|
||||
} else {
|
||||
assert.equal($('#consultation input:disabled').length, 3);
|
||||
assert.equal($('#consultation button').attr('type'), 'button');
|
||||
}
|
||||
assert.ok($('.qr-call-card a[href="tel:+14806877099"]').length);
|
||||
assert.equal($('#consultation-unavailable, [data-form-status-notice]').length, 0);
|
||||
assert.equal($('img[src^="https:"]').length, 0);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Guarded consultation-only Form A browser regression.
|
||||
* Run after an explicitly gated isolated-test build. Every Jotform request is
|
||||
* Run after a normal live build. Every Jotform request is
|
||||
* fulfilled or aborted locally; no Jotform request is transmitted.
|
||||
*/
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -95,8 +95,7 @@ try {
|
||||
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 adapter.getAttribute('data-form-mode'), 'live', 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);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 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 \
|
||||
* FORM_A_IFRAME_BROWSER_MODE=live \
|
||||
* PLAYWRIGHT_MODULE=/path/to/playwright/index.mjs \
|
||||
* CHROMIUM_EXECUTABLE=/path/to/chromium node tools/jotform-form-a-browser.mjs
|
||||
*/
|
||||
@@ -18,12 +18,11 @@ 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 mode = process.env.FORM_A_IFRAME_BROWSER_MODE ?? 'live';
|
||||
if (!['off', 'mock', 'live'].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 expectedIframe = mode === 'live';
|
||||
const expectedFormMode = 'live';
|
||||
const routes = [
|
||||
{ route: '/contact', locale: 'en', context: 'contact', screenshot: 'contact' },
|
||||
{ route: '/es/contact', locale: 'es', context: 'contact', screenshot: 'es-contact' },
|
||||
@@ -94,7 +93,6 @@ try {
|
||||
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'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* 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:
|
||||
* Build the ordinary live 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 \
|
||||
|
||||
@@ -1,167 +1,67 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
buildFormAIframeUrl,
|
||||
formAConsultationTestPlacements,
|
||||
formAIframeId,
|
||||
formAIframeOrigin,
|
||||
formAIframePlacements,
|
||||
formModeForRuntime,
|
||||
resolveFormAIframe,
|
||||
resolveFormMode,
|
||||
} from '../src/utils/form-integration.ts';
|
||||
import { buildFormAIframeUrl, formAIframeId, formAIframeOrigin, formAIframePlacements,
|
||||
formModeForRuntime, resolveFormAIframe, resolveFormMode } from '../src/utils/form-integration.ts';
|
||||
|
||||
const isolated = (overrides = {}) => ({
|
||||
rawMode: 'isolated-test',
|
||||
liveApproved: 'false',
|
||||
isolatedTestApproved: 'true',
|
||||
consultationTestApproved: 'false',
|
||||
productionApproved: 'false',
|
||||
environmentRole: 'isolated-test',
|
||||
allowIndexing: 'false',
|
||||
route: '/contact',
|
||||
locale: 'en',
|
||||
...overrides,
|
||||
});
|
||||
const expected = [
|
||||
['/contact', 'en', 'contact'], ['/es/contact', 'es', 'contact'],
|
||||
...['aba-therapy','aba-insurance','aba-ahcccs','aba-private-pay','early-intervention'].map(s => ['/lp/' + s, 'en', 'landing']),
|
||||
['/client-consultation', 'en', 'consultation'], ['/es/client-consultation', 'es', 'consultation'], ['/schedule-consultation', 'en', 'consultation'],
|
||||
...['en','es'].flatMap(lang => ['aba-therapy','autism-evaluations','learner-social-club'].map(s => [(lang === 'es' ? '/es/' : '/') + s, lang, 'consultation'])),
|
||||
];
|
||||
|
||||
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',
|
||||
route: '/contact',
|
||||
locale: 'en',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test('Form A iframe uses only fixed, approved route parameters', () => {
|
||||
test('default and explicit live enable exactly sixteen fixed route/locale/context placements', () => {
|
||||
assert.equal(formAIframeOrigin, 'https://form.jotform.com');
|
||||
assert.equal(formAIframeId, '262946078314059');
|
||||
assert.equal(
|
||||
buildFormAIframeUrl('en', 'contact'),
|
||||
'https://form.jotform.com/262946078314059?formContext=contact&language=en',
|
||||
);
|
||||
assert.equal(
|
||||
buildFormAIframeUrl('es', 'contact'),
|
||||
'https://form.jotform.com/262946078314059?formContext=contact&language=es',
|
||||
);
|
||||
assert.equal(
|
||||
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,');
|
||||
assert.equal(english.fallbackLinkText, 'open the form in a new tab.');
|
||||
assert.equal(spanish.fallbackText, 'Si el formulario integrado no se carga,');
|
||||
assert.equal(spanish.fallbackLinkText, 'abra el formulario en una pestaña nueva.');
|
||||
assert.deepEqual(formAIframePlacements.map((placement) => placement.route), [
|
||||
'/contact',
|
||||
'/es/contact',
|
||||
'/lp/aba-therapy',
|
||||
'/lp/aba-insurance',
|
||||
'/lp/aba-ahcccs',
|
||||
'/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', () => {
|
||||
const enabled = resolveFormAIframe(isolated());
|
||||
assert.equal(enabled.enabled, true);
|
||||
assert.equal(enabled.reason, 'enabled');
|
||||
assert.equal(enabled.title, 'AIA general enquiries form');
|
||||
assert.equal(enabled.mode, 'isolated-test');
|
||||
assert.equal(enabled.environmentRole, 'isolated-test');
|
||||
assert.equal(resolveFormAIframe(isolated({ rawMode: undefined })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(isolated({ rawMode: 'mock' })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(isolated({ liveApproved: undefined })).reason, 'live-approval-not-disabled');
|
||||
assert.equal(resolveFormAIframe(isolated({ liveApproved: 'true' })).reason, 'live-approval-not-disabled');
|
||||
assert.equal(resolveFormAIframe(isolated({ isolatedTestApproved: 'false' })).reason, 'isolated-test-not-approved');
|
||||
assert.equal(resolveFormAIframe(isolated({ environmentRole: 'staging' })).reason, 'invalid-environment-role');
|
||||
assert.equal(resolveFormAIframe(isolated({ allowIndexing: 'true' })).reason, 'indexing-enabled');
|
||||
assert.equal(resolveFormAIframe(isolated({ route: '/lp/early-intervention' })).url, 'https://form.jotform.com/262946078314059?formContext=landing&language=en');
|
||||
assert.equal(resolveFormAIframe(isolated({ route: '/es/contact', locale: 'en' })).reason, 'locale-not-authorized');
|
||||
assert.equal(resolveFormAIframe(isolated({ route: '/es/contact', locale: 'es' })).url, 'https://form.jotform.com/262946078314059?formContext=contact&language=es');
|
||||
const campaign = resolveFormAIframe(isolated({ route: '/lp/aba-therapy' }));
|
||||
assert.equal(campaign.url, 'https://form.jotform.com/262946078314059?formContext=landing&language=en');
|
||||
assert.equal(campaign.context, 'landing');
|
||||
const live = resolveFormAIframe(production());
|
||||
assert.equal(live.enabled, true);
|
||||
assert.equal(live.mode, 'live');
|
||||
assert.equal(live.environmentRole, 'production');
|
||||
assert.equal(live.url, 'https://form.jotform.com/262946078314059?formContext=contact&language=en');
|
||||
assert.equal(resolveFormAIframe(production({ productionApproved: undefined })).reason, 'production-approval-not-granted');
|
||||
assert.equal(resolveFormAIframe(production({ productionApproved: 'false' })).reason, 'production-approval-not-granted');
|
||||
assert.equal(resolveFormAIframe(production({ environmentRole: 'isolated-test' })).reason, 'invalid-environment-role');
|
||||
assert.equal(resolveFormAIframe(production({ liveApproved: 'false' })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(production({ rawMode: 'off' })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(production({ route: '/referrals' })).reason, 'route-not-authorized');
|
||||
assert.equal(resolveFormAIframe(production({ route: '/es/contact', locale: 'es' })).url, 'https://form.jotform.com/262946078314059?formContext=contact&language=es');
|
||||
assert.equal(formModeForRuntime(resolveFormMode('isolated-test', undefined)), 'off');
|
||||
assert.equal(formModeForRuntime(resolveFormMode('live', 'true')), 'off');
|
||||
});
|
||||
|
||||
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.deepEqual(formAIframePlacements.map(p => [p.route,p.locale,p.context]), expected);
|
||||
assert.equal(new Set(formAIframePlacements.map(p => p.route)).size, 16);
|
||||
for (const rawMode of [undefined, '', ' ', 'live', 'LIVE']) for (const [route, locale, context] of expected) {
|
||||
const config = resolveFormAIframe({ rawMode, route, locale });
|
||||
assert.equal(config.enabled, true, route);
|
||||
assert.equal(config.context, context);
|
||||
assert.equal(config.mode, 'live');
|
||||
assert.equal(config.url, `https://form.jotform.com/262946078314059?formContext=${context}&language=${locale}`);
|
||||
assert.equal(formModeForRuntime(resolveFormMode(rawMode)), 'off', 'native surfaces remain inactive');
|
||||
}
|
||||
|
||||
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');
|
||||
for (const value of [undefined, '', 'invalid']) assert.equal(resolveFormAIframe(consultation({ allowIndexing: value })).enabled, false);
|
||||
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', () => {
|
||||
test('invalid and obsolete mode values fail clearly instead of silently disabling forms', () => {
|
||||
for (const rawMode of ['isolated-test', 'production', 'invalid', 'false']) {
|
||||
assert.throws(() => resolveFormMode(rawMode), /PUBLIC_AIA_FORMS_MODE.*Remove.*live, off, or mock/);
|
||||
assert.throws(() => resolveFormAIframe({ rawMode, route: '/contact', locale: 'en' }), /PUBLIC_AIA_FORMS_MODE/);
|
||||
}
|
||||
});
|
||||
|
||||
test('off and mock reject real frames on every approved placement', () => {
|
||||
for (const rawMode of ['off', 'mock']) for (const [route, locale] of expected) {
|
||||
const config = resolveFormAIframe({ rawMode, route, locale });
|
||||
assert.equal(config.enabled, false);
|
||||
assert.equal(config.url, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
test('registry rejects unknown routes, mismatched locales and arbitrary URL parameters', () => {
|
||||
for (const route of ['/referrals', '/tour', '/ar/contact', '/contact/', '/contact?formContext=landing', 'https://evil.invalid']) {
|
||||
assert.equal(resolveFormAIframe({ rawMode: undefined, route, locale: 'en' }).reason, 'route-not-authorized');
|
||||
}
|
||||
for (const [route, locale] of expected) {
|
||||
assert.equal(resolveFormAIframe({ rawMode: undefined, route, locale: locale === 'en' ? 'es' : 'en' }).reason, 'locale-not-authorized');
|
||||
}
|
||||
assert.throws(() => buildFormAIframeUrl('ar', 'contact'), /Unsupported/);
|
||||
assert.throws(() => buildFormAIframeUrl('en', 'untrusted'), /Unsupported/);
|
||||
});
|
||||
|
||||
test('EN/ES consultation and contact titles/fallbacks remain localized', () => {
|
||||
const en = resolveFormAIframe({ rawMode: undefined, route: '/client-consultation', locale: 'en' });
|
||||
const es = resolveFormAIframe({ rawMode: undefined, route: '/es/client-consultation', locale: 'es' });
|
||||
assert.equal(en.title, 'AIA consultation form');
|
||||
assert.equal(es.title, 'Formulario de consulta de AIA');
|
||||
assert.equal(es.fallbackLinkText, 'abra el formulario de consulta en una pestaña nueva.');
|
||||
assert.equal(resolveFormAIframe({rawMode: undefined,route:'/es/contact',locale:'es'}).fallbackText,'Si el formulario integrado no se carga,');
|
||||
});
|
||||
|
||||
test('the shared adapter preserves layout, bilingual drawers, sandbox, fallback and dimensions', () => {
|
||||
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');
|
||||
@@ -173,11 +73,8 @@ test('the adapter preserves seven production routes and supports the separate bi
|
||||
assert.match(contact, /resolveFormAIframe/);
|
||||
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/);
|
||||
assert.match(campaign, /variant="embedded"/);
|
||||
assert.match(campaign, /\/lp\/aba-therapy/);
|
||||
assert.match(campaign, /const iframeDescription = 'Use this form to request a conversation\. An AIA team member can explain the next steps for your family\.'/);
|
||||
@@ -186,14 +83,10 @@ test('the adapter preserves seven production routes and supports the separate bi
|
||||
assert.match(earlyIntervention, /resolveFormAIframe/);
|
||||
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-consultation-test/);
|
||||
assert.match(drawer, /data-consultation-enabled/);
|
||||
assert.match(drawer, /serviceCopy.heading/);
|
||||
assert.match(drawer, /deferUntilActivated/);
|
||||
assert.match(fs.readFileSync('src/scripts/service-inquiry.ts', 'utf8'), /aia:jotform-activate/);
|
||||
@@ -203,7 +96,6 @@ test('the adapter preserves seven production routes and supports the separate bi
|
||||
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/);
|
||||
|
||||
@@ -495,7 +495,7 @@ before(async () => {
|
||||
await build({root:${JSON.stringify(temporary)},configFile:${JSON.stringify(path.relative(temporary, path.join(project, 'astro.config.mjs')))},logLevel:'silent',vite:{cacheDir:${JSON.stringify(path.join(temporary, '.vite'))}}});`;
|
||||
for (const mode of ['staging', 'indexing']) {
|
||||
execFileSync(process.execPath, ['--input-type=module', '-e', runner], {
|
||||
cwd: temporary, env: { ...process.env, PUBLIC_ALLOW_INDEXING: mode === 'indexing' ? 'true' : 'false' },
|
||||
cwd: temporary, env: { ...process.env, PUBLIC_AIA_FORMS_MODE: 'off', PUBLIC_ALLOW_INDEXING: mode === 'indexing' ? 'true' : 'false' },
|
||||
timeout: 90_000, maxBuffer: 4 * 1024 * 1024
|
||||
});
|
||||
const html = new Map();
|
||||
|
||||
@@ -36,7 +36,7 @@ function hasTerminalSlash(url) {
|
||||
return pathname !== '/' && pathname.endsWith('/');
|
||||
}
|
||||
|
||||
const formAProductionUrls = new Map([
|
||||
const formAContactLandingUrls = new Map([
|
||||
['/contact', 'https://form.jotform.com/262946078314059?formContext=contact&language=en'],
|
||||
['/es/contact', 'https://form.jotform.com/262946078314059?formContext=contact&language=es'],
|
||||
['/lp/aba-therapy', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
@@ -61,7 +61,7 @@ const formAConsultationDrawerUrls = new Map([
|
||||
['/es/learner-social-club', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
]);
|
||||
|
||||
const formAIsolatedStaticUrls = new Map([...formAProductionUrls, ...formAConsultationStaticUrls]);
|
||||
const formAStaticUrls = new Map([...formAContactLandingUrls, ...formAConsultationStaticUrls]);
|
||||
|
||||
function iframeSources(html) {
|
||||
// Astro correctly escapes query separators in serialized HTML attributes.
|
||||
@@ -81,7 +81,7 @@ function formAAdapters(html) {
|
||||
function unsafeSignals(html, allowApprovedFormA = false) {
|
||||
const signals = [];
|
||||
if (!allowApprovedFormA && /<iframe\b[^>]*\bsrc=["'][^"']*jotform/i.test(html)) signals.push('active Jotform iframe');
|
||||
if (!allowApprovedFormA && /262946078314059/i.test(html)) signals.push('isolated-test Form A identifier');
|
||||
if (!allowApprovedFormA && /262946078314059/i.test(html)) signals.push('disabled Form A identifier');
|
||||
if (/<script\b[^>]*\bsrc=["'][^"']*(?:jotform|googletagmanager|google-analytics|doubleclick)/i.test(html)) signals.push('active vendor tracking/form script');
|
||||
if (!allowApprovedFormA && /data-(?:form|forms)-mode=["']live["']/i.test(html)) signals.push('live form mode');
|
||||
if (/api\.azinstitute4autism\.com/i.test(html)) signals.push('production API origin');
|
||||
@@ -89,12 +89,13 @@ function unsafeSignals(html, allowApprovedFormA = false) {
|
||||
return signals;
|
||||
}
|
||||
|
||||
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();
|
||||
export async function verifyArtifact(root, mode = 'staging', forms = 'live') {
|
||||
if (!['staging', 'indexing'].includes(mode)) throw new Error('Artifact mode must be staging or indexing; form availability is selected separately with --forms=live|off|mock.');
|
||||
if (!['live', 'off', 'mock'].includes(forms)) throw new Error('Artifact forms must be live, off, or mock.');
|
||||
const formAEnabled = forms === 'live';
|
||||
const stagingPolicy = mode === 'staging';
|
||||
const staticFormAUrls = formAEnabled ? formAStaticUrls : new Map();
|
||||
const deferredFormAUrls = formAEnabled ? formAConsultationDrawerUrls : new Map();
|
||||
const htmlFiles = (await fg('**/*.html', { cwd: root, absolute: true })).sort();
|
||||
const routes = [];
|
||||
const canonicalMap = new Map();
|
||||
@@ -133,13 +134,21 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
}
|
||||
});
|
||||
const adapters = formAAdapters(html);
|
||||
for (const url of iframeSources(html)) {
|
||||
if (url !== expectedFormAUrl) errors.push(`${route}: unauthorized iframe source`);
|
||||
}
|
||||
for (const url of deferredIframeSources(html)) {
|
||||
if (url !== expectedDeferredFormAUrl) errors.push(`${route}: unauthorized deferred iframe source`);
|
||||
}
|
||||
if (expectedFormAUrl && deferredIframeSources(html).length) errors.push(`${route}: unexpected deferred frame`);
|
||||
if (expectedDeferredFormAUrl && iframeSources(html).length) errors.push(`${route}: drawer must not load before activation`);
|
||||
if (/<script[^>]*data-portal-payload[^>]*>\s*(?!null\s*<)[\s\S]*?\"streams\"[\s\S]*?<\/script>/i.test(html)) errors.push(`${route}: private portal ciphertext is not an approved release artifact`);
|
||||
if (!title) errors.push(`${route}: missing title`);
|
||||
if (!canonical) errors.push(`${route}: missing canonical`);
|
||||
if (!robots) errors.push(`${route}: missing robots directive`);
|
||||
if (trailingSlashUrls.length) errors.push(`${route}: root-relative URL(s) end in a slash: ${trailingSlashUrls.join(', ')}`);
|
||||
if (stagingPolicy && robots !== 'noindex,nofollow') errors.push(`${route}: staging robots is ${robots ?? 'missing'}`);
|
||||
if (production && robots === 'noindex,nofollow') errors.push(`${route}: production artifact is noindex`);
|
||||
if (!stagingPolicy && robots === 'noindex,nofollow') errors.push(`${route}: production artifact is noindex`);
|
||||
if (canonical) {
|
||||
const prior = canonicalMap.get(canonical);
|
||||
if (prior) errors.push(`${route}: duplicate canonical ${canonical} (also ${prior})`);
|
||||
@@ -150,18 +159,16 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
if (formAEnabled) {
|
||||
if (expectedFormAUrl) {
|
||||
if (jotformFrames.length !== 1 || jotformFrames[0] !== expectedFormAUrl) errors.push(`${route}: missing or invalid approved Form A iframe URL`);
|
||||
const expectedMode = production ? 'live' : 'isolated-test';
|
||||
const expectedEnvironment = production ? 'production' : 'isolated-test';
|
||||
if (adapters.length !== 1 || !adapters[0].includes(`data-form-mode="${expectedMode}"`) || !adapters[0].includes(`data-form-environment="${expectedEnvironment}"`)) {
|
||||
errors.push(`${route}: Form A adapter lacks the approved ${expectedMode} ${expectedEnvironment} gate`);
|
||||
if (adapters.length !== 1 || !adapters[0].includes('data-form-mode="live"')) {
|
||||
errors.push(`${route}: Form A adapter lacks live mode`);
|
||||
}
|
||||
formAIframes += jotformFrames.length;
|
||||
} 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`);
|
||||
if (adapters.length !== 1 || !adapters[0].includes('data-form-mode="live"') || !adapters[0].includes('data-jotform-deferred="true"')) {
|
||||
errors.push(`${route}: deferred consultation adapter lacks the live deferred configuration`);
|
||||
}
|
||||
deferredFormAIframes += deferredJotformFrames.length;
|
||||
} else if (jotformFrames.length || deferredJotformFrames.length || adapters.length || /262946078314059/i.test(html)) {
|
||||
@@ -205,14 +212,15 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
if (formAEnabled) {
|
||||
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 (unexpectedFormAFiles.length) errors.push(`Form A identifier appears outside approved routes: ${unexpectedFormAFiles.join(', ')}`);
|
||||
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(', ')}`);
|
||||
errors.push(`disabled Form A identifier appears in artifact: ${formAResourceFiles.join(', ')}`);
|
||||
}
|
||||
return {
|
||||
mode,
|
||||
forms,
|
||||
htmlRoutes: htmlFiles.length,
|
||||
sitemapUrls: sitemapRoutes.length,
|
||||
hreflangLinks,
|
||||
@@ -232,8 +240,8 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)) {
|
||||
const mode = process.argv.find((arg) => arg.startsWith('--mode='))?.slice('--mode='.length) ?? 'staging';
|
||||
const root = process.argv.find((arg) => arg.startsWith('--root='))?.slice('--root='.length) ?? 'dist';
|
||||
if (!['staging', 'indexing', 'isolated-test', 'production'].includes(mode)) throw new Error(`Unsupported mode: ${mode}`);
|
||||
const result = await verifyArtifact(path.resolve(root), mode);
|
||||
const forms = process.argv.find((arg) => arg.startsWith('--forms='))?.slice('--forms='.length) ?? 'live';
|
||||
const result = await verifyArtifact(path.resolve(root), mode, forms);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
if (!result.passed) process.exitCode = 1;
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { verifyArtifact } from './phase10-artifact-verify.mjs';
|
||||
|
||||
async function fixture(mode = 'staging') {
|
||||
async function fixture(mode = 'staging', forms = 'live') {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'aia-phase10-artifact-'));
|
||||
const productionFrames = [
|
||||
const contactLandingFrames = [
|
||||
['contact/index.html', 'https://form.jotform.com/262946078314059?formContext=contact&language=en'],
|
||||
['es/contact/index.html', 'https://form.jotform.com/262946078314059?formContext=contact&language=es'],
|
||||
['lp/aba-therapy/index.html', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
@@ -29,24 +29,21 @@ async function fixture(mode = 'staging') {
|
||||
['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'
|
||||
? productionFrames.map(([file, frame]) => [file, 'index,follow', `<section data-jotform-form-a data-form-mode="live" data-form-environment="production"><iframe src="${frame}"></iframe></section>`])
|
||||
: [['index.html', 'index,follow', '']];
|
||||
const robots = mode === 'staging' ? 'noindex,nofollow' : 'index,follow';
|
||||
const pages = [['index.html', robots, '']];
|
||||
if (mode === 'staging') pages.push(['lp/index.html', robots, '']);
|
||||
if (forms === 'live') {
|
||||
pages.push(...[...contactLandingFrames,...consultationFrames].map(([file, frame]) =>
|
||||
[file, robots, `<section data-jotform-form-a data-form-mode="live"><iframe src="${frame}"></iframe></section>`]));
|
||||
pages.push(...consultationDrawers.map(([file, frame]) =>
|
||||
[file, robots, `<section data-jotform-form-a data-form-mode="live" data-jotform-deferred="true"><iframe data-jotform-iframe-url="${frame}"></iframe></section>`]));
|
||||
}
|
||||
for (const [file, robots, body] of pages) {
|
||||
const target = path.join(root, file);
|
||||
await fs.mkdir(path.dirname(target), { recursive: true });
|
||||
await fs.writeFile(target, `<!doctype html><html><head><title>${file}</title><meta name="robots" content="${robots}"><link rel="canonical" href="https://www.azinstitute4autism.com/${file}"></head><body><main><h1>${file}</h1>${body}</main></body></html>`);
|
||||
}
|
||||
await fs.writeFile(path.join(root, 'sitemap.xml'), ['staging', 'isolated-test'].includes(mode) ? '<urlset></urlset>' : '<urlset><url><loc>https://www.azinstitute4autism.com/</loc></url></urlset>');
|
||||
await fs.writeFile(path.join(root, 'sitemap.xml'), mode === 'staging' ? '<urlset></urlset>' : '<urlset><url><loc>https://www.azinstitute4autism.com/</loc></url></urlset>');
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -64,28 +61,33 @@ test('indexing artifact requires sitemap and excludes review index', async () =>
|
||||
assert.equal(result.reviewIndex, false);
|
||||
});
|
||||
|
||||
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, 10);
|
||||
assert.equal(result.deferredFormAIframes, 6);
|
||||
assert.equal(result.sitemapUrls, 0);
|
||||
assert.equal(result.reviewIndex, true);
|
||||
test('live forms have ten immediate and six deferred placements independently of publication mode', async () => {
|
||||
for (const mode of ['staging','indexing']) {
|
||||
const result = await verifyArtifact(await fixture(mode), mode);
|
||||
assert.equal(result.passed, true, result.errors.join('; '));
|
||||
assert.equal(result.formAIframes, 10);
|
||||
assert.equal(result.deferredFormAIframes, 6);
|
||||
assert.equal(result.reviewIndex, mode === 'staging');
|
||||
}
|
||||
});
|
||||
|
||||
test('production artifact permits exactly the seven approved Form A iframe placements', async () => {
|
||||
const result = await verifyArtifact(await fixture('production'), 'production');
|
||||
assert.equal(result.passed, true);
|
||||
assert.equal(result.formAIframes, 7);
|
||||
assert.equal(result.reviewIndex, false);
|
||||
test('off and mock artifacts contain no real frames independently of publication mode', async () => {
|
||||
for (const mode of ['staging','indexing']) for (const forms of ['off','mock']) {
|
||||
const root = await fixture(mode, forms);
|
||||
const result = await verifyArtifact(root, mode, forms);
|
||||
assert.equal(result.passed, true);
|
||||
assert.equal(result.formAIframes + result.deferredFormAIframes, 0);
|
||||
await fs.appendFile(path.join(root, 'index.html'), '<iframe src="https://form.jotform.com/262946078314059"></iframe>');
|
||||
assert.equal((await verifyArtifact(root,mode,forms)).passed,false);
|
||||
}
|
||||
});
|
||||
|
||||
test('production artifact rejects an invalid Form A URL', async () => {
|
||||
const root = await fixture('production');
|
||||
test('indexing artifact rejects an invalid Form A URL', async () => {
|
||||
const root = await fixture('indexing');
|
||||
const contact = path.join(root, 'contact/index.html');
|
||||
const html = await fs.readFile(contact, 'utf8');
|
||||
await fs.writeFile(contact, html.replace('formContext=contact&language=en', 'formContext=landing&language=en'));
|
||||
const result = await verifyArtifact(root, 'production');
|
||||
const result = await verifyArtifact(root, 'indexing');
|
||||
assert.equal(result.passed, false);
|
||||
assert.ok(result.errors.some((error) => error.includes('invalid approved Form A iframe URL')));
|
||||
});
|
||||
@@ -98,7 +100,7 @@ test('staging artifact rejects active native form actions', async () => {
|
||||
assert.ok(result.errors.some((error) => error.includes('native form action')));
|
||||
});
|
||||
|
||||
test('artifact rejects live form mode and production API origin', async () => {
|
||||
test('artifact rejects production API origin even when Form A is approved', async () => {
|
||||
const root = await fixture('staging');
|
||||
await fs.appendFile(path.join(root, 'index.html'), '<div data-form-mode="live">https://api.azinstitute4autism.com</div>');
|
||||
const result = await verifyArtifact(root, 'staging');
|
||||
@@ -106,12 +108,12 @@ test('artifact rejects live form mode and production API origin', async () => {
|
||||
assert.ok(result.errors.some((error) => error.includes('unsafe artifact signals')));
|
||||
});
|
||||
|
||||
test('artifact rejects the isolated-test Form A identifier in any shipped text asset', async () => {
|
||||
test('artifact rejects Form A identity outside the exact approved HTML routes', async () => {
|
||||
const root = await fixture('staging');
|
||||
await fs.writeFile(path.join(root, 'assets.js'), 'https://form.jotform.com/262946078314059?formContext=contact');
|
||||
const result = await verifyArtifact(root, 'staging');
|
||||
assert.equal(result.passed, false);
|
||||
assert.ok(result.errors.some((error) => error.includes('isolated-test Form A identifier')));
|
||||
assert.ok(result.errors.some((error) => error.includes('outside approved routes')));
|
||||
});
|
||||
|
||||
test('artifact rejects root-relative URLs with terminal slashes', async () => {
|
||||
@@ -140,3 +142,22 @@ test('release verification rejects private portal resources and encrypted payloa
|
||||
assert.ok(result.errors.some((error) => error.includes('private portal ciphertext')));
|
||||
assert.ok(result.errors.some((error) => error.includes('private/legacy')));
|
||||
});
|
||||
|
||||
|
||||
test('artifact rejects extra routes, wrong origins and prematurely loaded drawers', async () => {
|
||||
const root = await fixture();
|
||||
await fs.appendFile(path.join(root,'index.html'), '<iframe src="https://form.jotform.com/262946078314059?formContext=contact&language=en"></iframe>');
|
||||
assert.equal((await verifyArtifact(root)).passed,false);
|
||||
const clean = await fixture();
|
||||
const file = path.join(clean,'aba-therapy/index.html');
|
||||
await fs.writeFile(file,(await fs.readFile(file,'utf8')).replace('data-jotform-iframe-url=', 'src='));
|
||||
assert.ok((await verifyArtifact(clean)).errors.some(e => e.includes('before activation')));
|
||||
const other = await fixture();
|
||||
await fs.appendFile(path.join(other,'index.html'), '<iframe src="https://unapproved.invalid/form"></iframe>');
|
||||
assert.ok((await verifyArtifact(other)).errors.some(e => e.includes('unauthorized iframe')));
|
||||
});
|
||||
|
||||
test('obsolete artifact modes are rejected with separate publication/form guidance', async () => {
|
||||
await assert.rejects(verifyArtifact('/unused','isolated-test'), /--forms=live\|off\|mock/);
|
||||
await assert.rejects(verifyArtifact('/unused','staging','invalid'), /live, off, or mock/);
|
||||
});
|
||||
@@ -13,7 +13,7 @@ const moduleName = process.env.PLAYWRIGHT_MODULE
|
||||
? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href : 'playwright';
|
||||
const executablePath = process.env.CHROMIUM_EXECUTABLE;
|
||||
const output = await fs.mkdtemp('/tmp/aia-phase10-browser-');
|
||||
const result = { startedAt: new Date().toISOString(), routeCount: 0, opened: 0, failures: [], pageErrors: [], consoleErrors: [], blocked: [], productionAttempts: [], screenshots: [], status: 'failed' };
|
||||
const result = { startedAt: new Date().toISOString(), routeCount: 0, opened: 0, failures: [], pageErrors: [], consoleErrors: [], blocked: [], productionAttempts: [], approvedFormAAttempts: [], screenshots: [], status: 'failed' };
|
||||
let browser;
|
||||
let preview;
|
||||
|
||||
@@ -59,7 +59,17 @@ try {
|
||||
if (url.origin !== origin || !['GET', 'HEAD'].includes(request.method())) {
|
||||
const attempt = { origin: url.origin, path: url.pathname, method: request.method() };
|
||||
result.blocked.push(attempt);
|
||||
if (isProductionLike(url)) result.productionAttempts.push(attempt);
|
||||
const parent = request.frame().parentFrame();
|
||||
const parentPath = parent ? new URL(parent.url()).pathname : '';
|
||||
const locale = parentPath.startsWith('/es/') ? 'es' : 'en';
|
||||
const contacts = ['/contact', '/es/contact'];
|
||||
const landings = ['/lp/aba-therapy', '/lp/aba-insurance', '/lp/aba-ahcccs', '/lp/aba-private-pay', '/lp/early-intervention'];
|
||||
const consultations = ['/client-consultation', '/es/client-consultation', '/schedule-consultation', '/aba-therapy', '/autism-evaluations', '/learner-social-club', '/es/aba-therapy', '/es/autism-evaluations', '/es/learner-social-club'];
|
||||
const formContext = contacts.includes(parentPath) ? 'contact' : landings.includes(parentPath) ? 'landing' : consultations.includes(parentPath) ? 'consultation' : undefined;
|
||||
const approved = request.method() === 'GET' && request.isNavigationRequest() && formContext &&
|
||||
url.href === `https://form.jotform.com/262946078314059?formContext=${formContext}&language=${locale}`;
|
||||
if (approved) result.approvedFormAAttempts.push(attempt);
|
||||
else if (isProductionLike(url)) result.productionAttempts.push(attempt);
|
||||
return route.abort();
|
||||
}
|
||||
return route.continue();
|
||||
|
||||
@@ -24,13 +24,13 @@ test('counter configuration defaults off and requires explicit approval', () =>
|
||||
assert.equal(resolveCounterConfig('mock', undefined, undefined).mode, 'mock');
|
||||
});
|
||||
|
||||
test('form configuration defaults off and live remains fail-closed', () => {
|
||||
assert.deepEqual(resolveFormMode(undefined, undefined), { mode: 'off', reason: 'default-off' });
|
||||
assert.equal(resolveFormMode('invalid', undefined).mode, 'off');
|
||||
assert.equal(resolveFormMode('live', 'false').mode, 'off');
|
||||
assert.equal(resolveFormMode('live', 'true').mode, 'live');
|
||||
assert.equal(formModeForRuntime(resolveFormMode('live', 'true')), 'off');
|
||||
assert.equal(resolveFormMode('mock', undefined).mode, 'mock');
|
||||
test('forms default live while native fixtures stay off and invalid settings throw', () => {
|
||||
assert.deepEqual(resolveFormMode(undefined), { mode: 'live', reason: 'default-live' });
|
||||
assert.throws(() => resolveFormMode('invalid'), /PUBLIC_AIA_FORMS_MODE/);
|
||||
assert.equal(resolveFormMode('live').mode, 'live');
|
||||
assert.equal(formModeForRuntime(resolveFormMode('live')), 'off');
|
||||
assert.equal(resolveFormMode('off').mode, 'off');
|
||||
assert.equal(resolveFormMode('mock').mode, 'mock');
|
||||
assert.equal(resolveMockFormResult(undefined), 'success');
|
||||
assert.equal(resolveMockFormResult('failed'), 'failed');
|
||||
assert.equal(resolveMockFormResult('unavailable'), 'unavailable');
|
||||
|
||||
@@ -336,7 +336,7 @@ await build({root:${JSON.stringify(temporary)},configFile:${JSON.stringify(path.
|
||||
}
|
||||
}
|
||||
execFileSync(process.execPath, ['--input-type=module', '-e', runner], {
|
||||
cwd: temporary, env: { ...process.env, PUBLIC_ALLOW_INDEXING: mode === 'staging' ? 'false' : 'true' },
|
||||
cwd: temporary, env: { ...process.env, PUBLIC_AIA_FORMS_MODE: '', PUBLIC_ALLOW_INDEXING: mode === 'staging' ? 'false' : 'true' },
|
||||
timeout: 60_000, maxBuffer: 2 * 1024 * 1024
|
||||
});
|
||||
const html = new Map();
|
||||
|
||||
@@ -39,7 +39,7 @@ async function writeEntry(collection, lang, slug, data = {}) {
|
||||
|
||||
function build(indexing = false) {
|
||||
return execFileSync(process.execPath, ['--input-type=module', '-e', runner], {
|
||||
cwd: temporary, env: { ...process.env, PUBLIC_ALLOW_INDEXING: String(indexing), TZ: 'America/Phoenix' },
|
||||
cwd: temporary, env: { ...process.env, PUBLIC_AIA_FORMS_MODE: 'off', PUBLIC_ALLOW_INDEXING: String(indexing), TZ: 'America/Phoenix' },
|
||||
timeout: 60_000, maxBuffer: 2 * 1024 * 1024, stdio: 'pipe'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Guarded consultation-only Form A browser regression.
|
||||
* Run after an explicitly gated isolated-test build. Every Jotform request is
|
||||
* Run after a normal live build. Every Jotform request is
|
||||
* fulfilled or aborted locally; no Jotform request is transmitted.
|
||||
*/
|
||||
import { randomBytes } from 'node:crypto';
|
||||
@@ -14,6 +14,8 @@ import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
|
||||
const forms = process.argv.find(a => a.startsWith('--forms='))?.split('=')[1] ?? 'live';
|
||||
assert.ok(['live', 'off', 'mock'].includes(forms));
|
||||
const root = fileURLToPath(new URL('../', import.meta.url));
|
||||
const moduleName = process.env.PLAYWRIGHT_MODULE
|
||||
? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href
|
||||
@@ -31,7 +33,7 @@ const routes = [
|
||||
{ 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 evidence = { forms, 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;
|
||||
@@ -93,7 +95,7 @@ try {
|
||||
return route.abort();
|
||||
});
|
||||
};
|
||||
if (!process.argv.includes('--portal-only') && !process.argv.includes('--correction-only')) {
|
||||
if (forms === 'live' && !process.argv.includes('--portal-only') && !process.argv.includes('--correction-only')) {
|
||||
for (const width of widths) {
|
||||
for (const target of routes.filter((r) => r.drawer)) {
|
||||
const context = await browser.newContext({ viewport: { width, height: 844 }, serviceWorkers: 'block', reducedMotion: 'reduce' });
|
||||
@@ -196,7 +198,7 @@ try {
|
||||
evidence.checks++;
|
||||
await backContext.close();
|
||||
}
|
||||
if (!process.argv.includes('--portal-only')) {
|
||||
if (forms === 'live' && !process.argv.includes('--portal-only')) {
|
||||
// Completion messages originate from a locally fulfilled cross-origin frame.
|
||||
// No fixture submits anything; malformed browser envelopes are injected only in tests.
|
||||
for (const locale of ['en', 'es']) {
|
||||
@@ -282,6 +284,35 @@ try {
|
||||
await ctx.close();
|
||||
}
|
||||
}
|
||||
if (forms !== 'live') {
|
||||
for (const width of [390, 1440]) for (const target of routes.filter(r => r.drawer)) {
|
||||
const ctx = await browser.newContext({ viewport: { width, height: 900 }, serviceWorkers: 'block', reducedMotion: 'reduce' });
|
||||
await guard(ctx);
|
||||
const p = await ctx.newPage();
|
||||
await p.clock.install({ time: new Date('2026-10-30T12:00:00Z') });
|
||||
await p.clock.pauseAt(new Date('2026-10-30T12:00:01Z'));
|
||||
await p.goto(origin + target.route, { waitUntil: 'domcontentloaded' });
|
||||
assert.equal(await p.locator('iframe[data-jotform-iframe-frame]').count(), 0);
|
||||
await p.evaluate(() => scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' }));
|
||||
await p.clock.runFor(11000);
|
||||
const dialog = p.locator('[data-service-inquiry-drawer]');
|
||||
if (forms === 'off') {
|
||||
assert.equal(await dialog.evaluate(d => d.open), false, 'off never auto-opens');
|
||||
const destination = target.locale === 'es' ? '/es/client-consultation' : '/client-consultation';
|
||||
const cta = p.locator('main a[href="' + destination + '"]').first();
|
||||
assert.ok(await cta.count());
|
||||
await cta.click();
|
||||
await p.waitForURL(origin + destination);
|
||||
} else {
|
||||
assert.equal(await dialog.evaluate(d => d.open), true, 'existing synthetic drawer remains available');
|
||||
assert.ok(await dialog.locator('[data-form-mode="mock"]').count());
|
||||
assert.equal(await dialog.locator('form').count(), 0);
|
||||
}
|
||||
assert.equal(evidence.jotformAttempts.length, 0);
|
||||
evidence.checks++;
|
||||
await ctx.close();
|
||||
}
|
||||
}
|
||||
// Actual portal UI with synthetic encrypted response fixtures, never real private source.
|
||||
for (const width of widths) {
|
||||
const ctx = await browser.newContext({ viewport: { width, height: 844 }, serviceWorkers: 'block' }); await guard(ctx, true);
|
||||
|
||||
@@ -58,9 +58,9 @@ test('four service configurations are bilingual without adding an Academy route'
|
||||
for (const variants of Object.values(serviceInquiryCopy)) for (const copy of Object.values(variants)) {
|
||||
assert.ok(copy.heading && copy.introduction); assert.ok(!copy.introduction.includes('—'));
|
||||
}
|
||||
const { formAConsultationTestPlacements } = await import('../src/utils/form-integration.ts');
|
||||
assert.equal(formAConsultationTestPlacements.length, 9);
|
||||
assert.ok(!formAConsultationTestPlacements.some((p) => p.route.includes('preparatory')));
|
||||
const { formAIframePlacements } = await import('../src/utils/form-integration.ts');
|
||||
assert.equal(formAIframePlacements.filter(p => p.context === 'consultation').length, 9);
|
||||
assert.ok(!formAIframePlacements.some((p) => p.route.includes('preparatory')));
|
||||
});
|
||||
test('legacy independent crypto fixture and two-view separation round-trip', async () => {
|
||||
const pass = randomBytes(32).toString('base64'), other = randomBytes(32).toString('base64');
|
||||
@@ -108,9 +108,14 @@ test('portal input requires every local gate and rejects missing or malformed ci
|
||||
const pass = randomBytes(24).toString('base64');
|
||||
const streams = await Promise.all(['one','two'].map((id) => encryptPortalStream(id, '<p>Synthetic</p>', pass)));
|
||||
const file = path.join(dir, 'fixture.json'); await fs.writeFile(file, JSON.stringify({ alg: portalAlgorithm, streams }), { mode: 0o600 });
|
||||
const options = { approved: 'true', indexing: 'false', environment: 'isolated-test', file };
|
||||
const options = { approved: 'true', indexing: 'false', file };
|
||||
assert.ok(readPortalInput(options));
|
||||
for (const changed of [{ approved: undefined }, { approved: 'false' }, { indexing: 'true' }, { indexing: undefined }, { environment: 'production' }, { file: '/missing-fixture' }]) assert.equal(readPortalInput({ ...options, ...changed }), undefined);
|
||||
// Form modes cannot substitute for the independent private-input approval.
|
||||
for (const mode of ['live', 'off', 'mock']) {
|
||||
assert.ok(readPortalInput({ ...options, mode }));
|
||||
assert.equal(readPortalInput({ indexing: 'false', file, mode }), undefined);
|
||||
}
|
||||
for (const changed of [{ approved: undefined }, { approved: 'false' }, { indexing: 'true' }, { indexing: undefined }, { file: '/missing-fixture' }]) assert.equal(readPortalInput({ ...options, ...changed }), undefined);
|
||||
await fs.writeFile(file, '<h1>Not encrypted</h1>'); assert.equal(readPortalInput(options), undefined);
|
||||
} finally { await fs.rm(dir, { recursive: true }); }
|
||||
});
|
||||
Reference in new issue
Block a user