feat(forms): add Form A iframe fallback safeguards
This commit is contained in:
10 files changed
+299
-59
No files matched your search
@@ -33,7 +33,7 @@ const routes = [
|
||||
];
|
||||
const widths = [320, 390, 768, 1440];
|
||||
const output = await fs.mkdtemp('/tmp/aia-form-a-iframe-');
|
||||
const evidence = { mode, routes, widths, checks: 0, blocked: [], jotformAttempts: [], screenshots: [], status: 'failed' };
|
||||
const evidence = { mode, routes, widths, checks: 0, fallbackChecks: [], blocked: [], jotformAttempts: [], 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;
|
||||
@@ -103,12 +103,15 @@ try {
|
||||
const adapterBox = await page.locator('[data-jotform-form-a]').boundingBox();
|
||||
assert.ok(copyBox && adapterBox && Math.abs(copyBox.width - adapterBox.width) <= 1, `${route} should use the full desktop grid column`);
|
||||
}
|
||||
const fallbackMarkup = await iframe.evaluate((element) => element.innerHTML);
|
||||
assert.match(fallbackMarkup, /class="jotform-iframe__fallback"/, route);
|
||||
const encodedFallbackUrl = expectedUrl.replace(/&/g, '&');
|
||||
assert.match(fallbackMarkup, new RegExp(`href="${encodedFallbackUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`), route);
|
||||
assert.match(fallbackMarkup, /target="_blank" rel="noopener noreferrer"/, route);
|
||||
assert.equal(await page.locator('[data-jotform-form-a] > .jotform-iframe__fallback').count(), 0, `${route} should not show a duplicate parent fallback`);
|
||||
const fallback = page.locator('[data-jotform-form-a] > [data-jotform-iframe-fallback]');
|
||||
assert.equal(await fallback.count(), 1, `${route} should render one parent fallback`);
|
||||
assert.equal(await fallback.getAttribute('hidden'), '', `${route} fallback should not reserve space before timeout`);
|
||||
assert.equal(await fallback.getAttribute('aria-live'), 'polite', route);
|
||||
assert.equal(await fallback.locator('a').getAttribute('href'), expectedUrl, route);
|
||||
assert.equal(await fallback.locator('a').getAttribute('target'), '_blank', route);
|
||||
assert.equal(await fallback.locator('a').getAttribute('rel'), 'noopener noreferrer', route);
|
||||
assert.equal(await iframe.evaluate((element) => element.innerHTML.trim()), '', `${route} should not keep fallback content inside the iframe`);
|
||||
assert.equal(await page.locator('[data-jotform-form-a] .jotform-iframe__fallback--noscript').count(), 0, `${route} should not render a duplicate noscript fallback while JavaScript is enabled`);
|
||||
if (route.startsWith('/lp/')) {
|
||||
const expectedSurface = route === '/lp/early-intervention'
|
||||
? '#consultation.lead-form > [data-jotform-form-a].jotform-iframe--embedded'
|
||||
@@ -143,17 +146,24 @@ try {
|
||||
if (url.origin === 'https://form.jotform.com') evidence.jotformAttempts.push(item);
|
||||
return route.abort();
|
||||
});
|
||||
for (const { route } of routes) {
|
||||
for (const { route, locale } of routes) {
|
||||
evidence.checks++;
|
||||
const page = await context.newPage();
|
||||
const response = await page.goto(origin + route, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||
assert.equal(response?.status(), 200, `${route} without JavaScript`);
|
||||
assert.equal(await page.locator('[data-jotform-form-a] iframe').count(), 1, `${route} without JavaScript`);
|
||||
const iframe = page.locator('[data-jotform-form-a] iframe');
|
||||
const fallbackMarkup = await iframe.evaluate((element) => element.innerHTML);
|
||||
assert.match(fallbackMarkup, /class="jotform-iframe__fallback"/, `${route} without JavaScript`);
|
||||
assert.match(fallbackMarkup, /target="_blank" rel="noopener noreferrer"/, `${route} without JavaScript`);
|
||||
assert.equal(await page.locator('[data-jotform-form-a] > .jotform-iframe__fallback').count(), 0, `${route} without JavaScript should not show a duplicate parent fallback`);
|
||||
assert.equal(await iframe.evaluate((element) => element.innerHTML.trim()), '', `${route} without JavaScript should not place fallback content inside the iframe`);
|
||||
const noscriptFallback = page.locator('[data-jotform-form-a] .jotform-iframe__fallback--noscript');
|
||||
assert.equal(await noscriptFallback.count(), 1, `${route} without JavaScript should expose one localized hosted-form fallback`);
|
||||
assert.equal(await noscriptFallback.isVisible(), true, `${route} without JavaScript fallback should be visible`);
|
||||
assert.equal(await noscriptFallback.locator('a').getAttribute('target'), '_blank', `${route} without JavaScript`);
|
||||
assert.equal(await noscriptFallback.locator('a').getAttribute('rel'), 'noopener noreferrer', `${route} without JavaScript`);
|
||||
assert.match(
|
||||
await noscriptFallback.innerText(),
|
||||
locale === 'es' ? /Si el formulario integrado no se carga,/ : /If the embedded form does not load,/,
|
||||
`${route} without JavaScript should use the localized fallback`,
|
||||
);
|
||||
if (route.startsWith('/lp/')) {
|
||||
const expectedSurface = route === '/lp/early-intervention'
|
||||
? '#consultation.lead-form > [data-jotform-form-a].jotform-iframe--embedded'
|
||||
@@ -165,6 +175,80 @@ try {
|
||||
await context.close();
|
||||
}
|
||||
|
||||
if (expectedIframe) {
|
||||
// Exercise the first-party fallback with a local fixture. Jotform is never
|
||||
// contacted: every frame request is fulfilled or held inside Playwright.
|
||||
let fallbackScenario = 'immediate';
|
||||
let releaseLateFrame;
|
||||
let timeoutFrameRequested;
|
||||
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, serviceWorkers: 'block' });
|
||||
await context.route('**/*', async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
if (url.origin === origin && ['GET', 'HEAD'].includes(request.method())) return route.continue();
|
||||
const item = { origin: url.origin, path: url.pathname, search: url.search, method: request.method(), fixture: true };
|
||||
evidence.blocked.push(item);
|
||||
if (url.origin === 'https://form.jotform.com' && url.pathname === '/262946078314059') {
|
||||
evidence.jotformAttempts.push(item);
|
||||
if (fallbackScenario === 'delayed') await delay(30);
|
||||
if (fallbackScenario === 'timeout-late') {
|
||||
timeoutFrameRequested?.();
|
||||
await new Promise((resolve) => { releaseLateFrame = resolve; });
|
||||
}
|
||||
return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>Local Form A Fixture</title><p>Fixture only</p>' });
|
||||
}
|
||||
return route.abort();
|
||||
});
|
||||
|
||||
const openFallbackScenario = async (name) => {
|
||||
const page = await context.newPage();
|
||||
// Keep the production 15-second timeout literal while making this local
|
||||
// fixture deterministic and fast. The override is scoped to the page.
|
||||
await page.addInitScript(() => {
|
||||
const originalSetTimeout = window.setTimeout.bind(window);
|
||||
window.setTimeout = (callback, timeout, ...args) => originalSetTimeout(callback, timeout === 15_000 ? 80 : timeout, ...args);
|
||||
});
|
||||
const response = await page.goto(origin + '/contact', { waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||
assert.equal(response?.status(), 200, `fallback ${name} outer page`);
|
||||
return { page, fallback: page.locator('[data-jotform-iframe-fallback]') };
|
||||
};
|
||||
|
||||
fallbackScenario = 'immediate';
|
||||
{
|
||||
const { page, fallback } = await openFallbackScenario('immediate');
|
||||
await page.waitForTimeout(30);
|
||||
assert.equal(await fallback.isVisible(), false, 'A successful iframe load keeps fallback hidden');
|
||||
evidence.fallbackChecks.push('successful-load-hidden');
|
||||
await page.close();
|
||||
}
|
||||
|
||||
fallbackScenario = 'delayed';
|
||||
{
|
||||
const { page, fallback } = await openFallbackScenario('delayed');
|
||||
await page.waitForTimeout(45);
|
||||
assert.equal(await fallback.isVisible(), false, 'A delayed iframe load stays hidden before the 15-second timeout');
|
||||
evidence.fallbackChecks.push('delayed-load-hidden-before-timeout');
|
||||
await page.close();
|
||||
}
|
||||
|
||||
fallbackScenario = 'timeout-late';
|
||||
{
|
||||
const requested = new Promise((resolve) => { timeoutFrameRequested = resolve; });
|
||||
const { page, fallback } = await openFallbackScenario('timeout-late');
|
||||
await requested;
|
||||
await page.waitForTimeout(100);
|
||||
assert.equal(await fallback.isVisible(), true, 'A missing iframe load reveals the accessible hosted-form fallback after timeout');
|
||||
assert.equal(await fallback.getAttribute('role'), 'status');
|
||||
releaseLateFrame?.();
|
||||
await page.waitForFunction(() => document.querySelector('[data-jotform-iframe-fallback]')?.hidden === true);
|
||||
assert.equal(await fallback.isVisible(), false, 'A late iframe load hides the timeout fallback consistently');
|
||||
evidence.fallbackChecks.push('timeout-visible', 'late-load-hidden');
|
||||
await page.close();
|
||||
}
|
||||
|
||||
await context.close();
|
||||
}
|
||||
|
||||
const formAttempts = evidence.jotformAttempts;
|
||||
if (expectedIframe) {
|
||||
assert.ok(formAttempts.length >= routes.length, 'Expected intercepted Jotform iframe GET attempts');
|
||||
|
||||
@@ -37,6 +37,12 @@ test('Form A iframe uses only fixed, approved route parameters', () => {
|
||||
buildFormAIframeUrl('en', 'landing'),
|
||||
'https://form.jotform.com/262946078314059?formContext=landing&language=en',
|
||||
);
|
||||
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',
|
||||
@@ -56,6 +62,8 @@ test('Form A iframe requires every isolated-test gate and refuses live mode', ()
|
||||
assert.equal(resolveFormAIframe(isolated({ rawMode: undefined })).reason, 'mode-not-isolated-test');
|
||||
assert.equal(resolveFormAIframe(isolated({ rawMode: 'mock' })).reason, 'mode-not-isolated-test');
|
||||
assert.equal(resolveFormAIframe(isolated({ rawMode: 'live', liveApproved: 'true' })).reason, 'mode-not-isolated-test');
|
||||
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');
|
||||
@@ -90,10 +98,13 @@ test('Only the seven allowlisted placements can render the adapter, with no help
|
||||
assert.match(earlyIntervention, /JotformIframe/);
|
||||
assert.match(adapter, /referrerpolicy="no-referrer"/);
|
||||
assert.match(adapter, /sandbox="allow-forms allow-scripts allow-same-origin"/);
|
||||
assert.match(adapter, /const fallbackMarkup = `<p class="jotform-iframe__fallback">/);
|
||||
assert.match(adapter, /set:html=\{fallbackMarkup\}/);
|
||||
assert.match(adapter, /escapeHtml\(config\.url\)/);
|
||||
assert.doesNotMatch(adapter, /<\/h3>\s*(?:\{description[^\n]*\}\s*)?<p class="jotform-iframe__fallback">/);
|
||||
assert.match(adapter, /data-jotform-iframe-frame/);
|
||||
assert.match(adapter, /data-jotform-iframe-fallback hidden aria-live="polite" role="status"/);
|
||||
assert.match(adapter, /<noscript>/);
|
||||
assert.match(adapter, /fallbackTimeoutMs = 15_000/);
|
||||
assert.match(adapter, /frame\.addEventListener\('load', onLoad, \{ once: true \}\)/);
|
||||
assert.match(adapter, /document\.addEventListener\('astro:before-swap', dispose, \{ once: true \}\)/);
|
||||
assert.doesNotMatch(adapter, /set:html=|fallbackMarkup|escapeHtml\(/);
|
||||
assert.match(adapter, /target="_blank" rel="noopener noreferrer"/);
|
||||
assert.match(adapter, /--jotform-frame-height: 60rem/);
|
||||
assert.match(adapter, /max-width: none;/);
|
||||
@@ -101,7 +112,7 @@ test('Only the seven allowlisted placements can render the adapter, with no help
|
||||
assert.match(adapter, /\.jotform-iframe--embedded \{\s*--jotform-frame-height: 42rem/);
|
||||
assert.match(adapter, /\.jotform-iframe__frame \{\s*height: 68rem/);
|
||||
assert.match(adapter, /\.jotform-iframe--embedded \.jotform-iframe__frame \{\s*height: 50rem/);
|
||||
assert.doesNotMatch(adapter, /<script|onload=|postMessage|fetch\(|form\.submit|analytics|googletag/i);
|
||||
assert.doesNotMatch(adapter, /for-form-embed-handler|jotformEmbedHandler|onload=|postMessage|fetch\(|form\.submit|analytics|googletag/i);
|
||||
assert.doesNotMatch(campaign, /262946078314059/);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user