feat(forms): add isolated Form A consultation testing
This commit is contained in:
25 files changed
+1046
-253
No files matched your search
@@ -46,12 +46,34 @@ const formAProductionUrls = new Map([
|
||||
['/lp/early-intervention', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
]);
|
||||
|
||||
const formAConsultationStaticUrls = new Map([
|
||||
['/client-consultation', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
['/es/client-consultation', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
['/schedule-consultation', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
]);
|
||||
|
||||
const formAConsultationDrawerUrls = new Map([
|
||||
['/aba-therapy', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
['/autism-evaluations', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
['/learner-social-club', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||
['/es/aba-therapy', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
['/es/autism-evaluations', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
['/es/learner-social-club', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
]);
|
||||
|
||||
const formAIsolatedStaticUrls = new Map([...formAProductionUrls, ...formAConsultationStaticUrls]);
|
||||
|
||||
function iframeSources(html) {
|
||||
// Astro correctly escapes query separators in serialized HTML attributes.
|
||||
// Normalize that representation before comparing the fixed allowlisted URL.
|
||||
return [...html.matchAll(/<iframe\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi)].map((match) => match[1].replaceAll('&', '&'));
|
||||
}
|
||||
|
||||
function deferredIframeSources(html) {
|
||||
return [...html.matchAll(/<iframe\b[^>]*\bdata-jotform-iframe-url=["']([^"']+)["'][^>]*>/gi)]
|
||||
.map((match) => match[1].replaceAll('&', '&'));
|
||||
}
|
||||
|
||||
function formAAdapters(html) {
|
||||
return [...html.matchAll(/<section\b[^>]*\bdata-jotform-form-a\b[^>]*>/gi)].map((match) => match[0]);
|
||||
}
|
||||
@@ -71,6 +93,8 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
const formAEnabled = mode === 'isolated-test' || mode === 'production';
|
||||
const production = mode === 'production';
|
||||
const stagingPolicy = mode === 'staging' || mode === 'isolated-test';
|
||||
const staticFormAUrls = production ? formAProductionUrls : mode === 'isolated-test' ? formAIsolatedStaticUrls : new Map();
|
||||
const deferredFormAUrls = mode === 'isolated-test' ? formAConsultationDrawerUrls : new Map();
|
||||
const htmlFiles = (await fg('**/*.html', { cwd: root, absolute: true })).sort();
|
||||
const routes = [];
|
||||
const canonicalMap = new Map();
|
||||
@@ -80,6 +104,7 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
let activeForms = 0;
|
||||
let secretSignals = [];
|
||||
let formAIframes = 0;
|
||||
let deferredFormAIframes = 0;
|
||||
|
||||
for (const file of htmlFiles) {
|
||||
const html = await readText(file);
|
||||
@@ -90,7 +115,8 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
const actions = formActions(html);
|
||||
const nonSearchActions = actions.filter((action) => action && action !== '/search');
|
||||
const trailingSlashUrls = rootRelativeUrls(html).filter(hasTerminalSlash);
|
||||
const expectedFormAUrl = formAEnabled ? formAProductionUrls.get(route) : undefined;
|
||||
const expectedFormAUrl = formAEnabled ? staticFormAUrls.get(route) : undefined;
|
||||
const expectedDeferredFormAUrl = formAEnabled ? deferredFormAUrls.get(route) : undefined;
|
||||
const signals = unsafeSignals(html, formAEnabled);
|
||||
const jotformFrames = iframeSources(html).filter((src) => {
|
||||
try {
|
||||
@@ -99,6 +125,13 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const deferredJotformFrames = deferredIframeSources(html).filter((src) => {
|
||||
try {
|
||||
return new URL(src).origin === 'https://form.jotform.com';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const adapters = formAAdapters(html);
|
||||
if (!title) errors.push(`${route}: missing title`);
|
||||
if (!canonical) errors.push(`${route}: missing canonical`);
|
||||
@@ -122,8 +155,16 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
errors.push(`${route}: Form A adapter lacks the approved ${expectedMode} ${expectedEnvironment} gate`);
|
||||
}
|
||||
formAIframes += jotformFrames.length;
|
||||
} else if (jotformFrames.length || adapters.length || /262946078314059/i.test(html)) {
|
||||
errors.push(`${route}: unauthorized Form A iframe or identifier in production artifact`);
|
||||
} else if (expectedDeferredFormAUrl) {
|
||||
if (jotformFrames.length || deferredJotformFrames.length !== 1 || deferredJotformFrames[0] !== expectedDeferredFormAUrl) {
|
||||
errors.push(`${route}: missing or invalid deferred consultation Form A iframe URL`);
|
||||
}
|
||||
if (adapters.length !== 1 || !adapters[0].includes('data-form-mode="isolated-test"') || !adapters[0].includes('data-form-environment="isolated-test"') || !adapters[0].includes('data-jotform-deferred="true"')) {
|
||||
errors.push(`${route}: deferred consultation adapter lacks the approved isolated-test gate`);
|
||||
}
|
||||
deferredFormAIframes += deferredJotformFrames.length;
|
||||
} else if (jotformFrames.length || deferredJotformFrames.length || adapters.length || /262946078314059/i.test(html)) {
|
||||
errors.push(`${route}: unauthorized Form A iframe or identifier in ${mode} artifact`);
|
||||
}
|
||||
}
|
||||
if (nonSearchActions.length) activeForms += nonSearchActions.length;
|
||||
@@ -160,10 +201,11 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
digest.update('\n');
|
||||
}
|
||||
if (formAEnabled) {
|
||||
const expectedFormAFiles = new Set([...formAProductionUrls.keys()].map((route) => route === '/' ? 'index.html' : `${route.slice(1)}/index.html`));
|
||||
const expectedFormAFiles = new Set([...staticFormAUrls.keys(), ...deferredFormAUrls.keys()].map((route) => route === '/' ? 'index.html' : `${route.slice(1)}/index.html`));
|
||||
const unexpectedFormAFiles = formAResourceFiles.filter((file) => !expectedFormAFiles.has(file));
|
||||
if (unexpectedFormAFiles.length) errors.push(`Form A identifier appears outside approved production routes: ${unexpectedFormAFiles.join(', ')}`);
|
||||
if (formAIframes !== formAProductionUrls.size) errors.push(`production artifact has ${formAIframes} approved Form A iframe(s), expected ${formAProductionUrls.size}`);
|
||||
if (formAIframes !== staticFormAUrls.size) errors.push(`${mode} artifact has ${formAIframes} approved static Form A iframe(s), expected ${staticFormAUrls.size}`);
|
||||
if (deferredFormAIframes !== deferredFormAUrls.size) errors.push(`${mode} artifact has ${deferredFormAIframes} approved deferred Form A iframe(s), expected ${deferredFormAUrls.size}`);
|
||||
} else if (formAResourceFiles.length) {
|
||||
errors.push(`isolated-test Form A identifier appears in artifact: ${formAResourceFiles.join(', ')}`);
|
||||
}
|
||||
@@ -175,6 +217,7 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
reviewIndex,
|
||||
activeNativeFormActions: activeForms,
|
||||
formAIframes,
|
||||
deferredFormAIframes,
|
||||
artifactFiles: files.length,
|
||||
artifactSha256: digest.digest('hex'),
|
||||
routes,
|
||||
|
||||
Reference in new issue
Block a user