464 lines
32 KiB
JavaScript
464 lines
32 KiB
JavaScript
/**
|
||
* Guarded consultation-only Form A browser regression.
|
||
* Run after a normal live build. Every Jotform request is
|
||
* fulfilled or aborted locally; no Jotform request is transmitted.
|
||
*/
|
||
import { randomBytes } from 'node:crypto';
|
||
import { encryptPortalStream, portalAlgorithm } from '../src/utils/portal-crypto.mjs';
|
||
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 forms = process.argv.find(a => a.startsWith('--forms='))?.split('=')[1] ?? 'live';
|
||
assert.ok(['live', 'off', 'mock'].includes(forms));
|
||
const browserName = process.argv.find(a => a.startsWith('--browser='))?.split('=')[1] ?? 'chromium';
|
||
assert.ok(['chromium', 'firefox'].includes(browserName));
|
||
const root = fileURLToPath(new URL('../', import.meta.url));
|
||
const moduleName = process.env.PLAYWRIGHT_MODULE
|
||
? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href
|
||
: 'playwright';
|
||
const output = await fs.mkdtemp('/tmp/aia-service-portal-');
|
||
const routes = [
|
||
{ route: '/client-consultation', locale: 'en', drawer: false, screenshot: 'client-consultation' },
|
||
{ route: '/es/client-consultation', locale: 'es', drawer: false, screenshot: 'es-client-consultation' },
|
||
{ route: '/schedule-consultation', locale: 'en', drawer: false, screenshot: 'schedule-consultation' },
|
||
{ route: '/aba-therapy', locale: 'en', drawer: true, screenshot: 'aba-therapy' },
|
||
{ route: '/autism-evaluations', locale: 'en', drawer: true, screenshot: 'autism-evaluations' },
|
||
{ route: '/learner-social-club', locale: 'en', drawer: true, screenshot: 'learner-social-club' },
|
||
{ route: '/es/aba-therapy', locale: 'es', drawer: true, screenshot: 'es-aba-therapy' },
|
||
{ route: '/es/autism-evaluations', locale: 'es', drawer: true, screenshot: 'es-autism-evaluations' },
|
||
{ route: '/es/learner-social-club', locale: 'es', drawer: true, screenshot: 'es-learner-social-club' },
|
||
];
|
||
const widths = [320, 390, 768, 1440];
|
||
const evidence = { forms, browserName, routes, widths, checks: 0, blocked: [], jotformAttempts: [], resourceRequests: [], 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;
|
||
await new Promise((resolve) => reserve.close(resolve));
|
||
const origin = 'http://127.0.0.1:' + port;
|
||
let browser;
|
||
let preview;
|
||
|
||
function expectedUrl(locale) {
|
||
return 'https://form.jotform.com/262946078314059?formContext=consultation&language=' + locale;
|
||
}
|
||
|
||
try {
|
||
const drivers = await import(moduleName);
|
||
const driver = drivers[browserName];
|
||
const executablePath = browserName === 'chromium' ? process.env.CHROMIUM_EXECUTABLE : process.env.FIREFOX_EXECUTABLE;
|
||
browser = await driver.launch(executablePath
|
||
? { headless: true, executablePath }
|
||
: { headless: true });
|
||
evidence.browser = browser.version();
|
||
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, 'Loopback preview did not become reachable');
|
||
|
||
|
||
const key = 'aia:service-inquiry:auto-suppressed-until';
|
||
const duration = 30 * 86400000;
|
||
const staffCode = randomBytes(24).toString('hex');
|
||
const managementCode = randomBytes(24).toString('hex');
|
||
const staffResourceName = 'a'.repeat(64) + '.pdf';
|
||
const managementResourceName = 'b'.repeat(64) + '.docx';
|
||
const staffResourcePath = `/staff-resources/${staffResourceName}`;
|
||
const managementResourcePath = `/management-resources/${managementResourceName}`;
|
||
const styledFixture = '<section><h2>Synthetic Directory</h2><h3>Operational reports</h3><p><a class="hs-button arbitrary-legacy-class" style="position:fixed" onclick="window.portalUnsafe=true" href="https://example.invalid/report">Synthetic report</a></p><ul><li>Synthetic list item</li></ul><table><caption>Fixture directory</caption><thead><tr><th scope="col">Role</th><th scope="col">Contact</th></tr></thead><tbody><tr><td>Fixture role</td><td><a href="mailto:fixture@example.invalid">Fixture email</a></td></tr></tbody></table><p><a style="background-color:#ffbd59" href="https://example.invalid/second">Second fixture report</a></p><p><a class="unrecognized-report-legacy" href="https://example.invalid/third">Operations Monthly Lineup Report - PM</a></p><a href="javascript:alert(1)">Unsafe destination</a><iframe src="https://example.invalid/frame"></iframe><form action="https://example.invalid/post"><input></form></section>';
|
||
const staffTemplates = '<section><h2>Management Templates</h2><ul><li><div><a href="https://example.invalid/check-in">Client Check-In & Check-Out</a></div></li><li><div><a href="https://example.invalid/employee-performance">Employee Performance Template</a></div></li><li><div><a href="https://example.invalid/employee-availability">Employee Availability Template</a></div></li></ul></section><section><h2>Other Staff Resources</h2><p><a href="https://example.invalid/resource">General staff resource</a></p><p><a href="mailto:staff@example.invalid">Staff email</a></p></section>';
|
||
const payload = { alg: portalAlgorithm, streams: [
|
||
await encryptPortalStream('b', `<h2>Synthetic Staff</h2><p>Staff fixture only</p><a href="${staffResourcePath}">Staff's Safety & Training: Résumé (2026)!</a><script>window.portalUnsafe=true</script><img src="https://example.invalid/image">` + styledFixture + staffTemplates, staffCode),
|
||
await encryptPortalStream('a', `<h2>Synthetic Management</h2><p>Management fixture only</p><a class="hs-button" href="${managementResourcePath}">Management – Policies / Q&A #1</a>` + styledFixture, managementCode),
|
||
] };
|
||
const expectedAuthorization = {
|
||
staff: 'Basic ' + Buffer.from('staff:' + staffCode, 'utf8').toString('base64'),
|
||
management: 'Basic ' + Buffer.from('management:' + managementCode, 'utf8').toString('base64'),
|
||
};
|
||
const guard = async (context, inject = false) => {
|
||
await context.routeWebSocket('**/*', (socket) => { evidence.blocked.push({ method: 'WEBSOCKET' }); socket.close(); });
|
||
await context.route('**/*', async (route) => {
|
||
const req = route.request(), url = new URL(req.url());
|
||
if (url.origin === origin && ['GET', 'HEAD'].includes(req.method())) {
|
||
const resource = /^\/(staff|management)-resources\/([a-f0-9]{64})[.](pdf|docx)$/.exec(url.pathname);
|
||
if (resource) {
|
||
const role = resource[1];
|
||
const authorized = req.headers().authorization === expectedAuthorization[role];
|
||
evidence.resourceRequests.push({ role, authorized });
|
||
if (!authorized) return route.fulfill({ status: 401, headers: { 'www-authenticate': `Basic realm="AIA ${role} resources"`, 'cache-control': 'private, no-store' } });
|
||
return route.fulfill({
|
||
status: 200,
|
||
contentType: resource[3] === 'pdf' ? 'application/pdf' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||
headers: { 'cache-control': 'private, no-store' },
|
||
body: resource[3] === 'pdf' ? `%PDF-1.7\nSynthetic ${role} document\n` : `Synthetic ${role} document\n`,
|
||
});
|
||
}
|
||
if (inject && url.pathname === '/employee-portal') {
|
||
const response = await route.fetch();
|
||
const html = (await response.text()).replace(/(<script[^>]*data-portal-payload[^>]*>)[\s\S]*?(<\/script>)/, '$1' + JSON.stringify(payload) + '$2');
|
||
return route.fulfill({ response, body: html });
|
||
}
|
||
return route.continue();
|
||
}
|
||
evidence.blocked.push({ method: req.method(), origin: url.origin, path: url.pathname });
|
||
if (url.origin === 'https://form.jotform.com') {
|
||
evidence.jotformAttempts.push({ method: req.method(), url: url.href });
|
||
return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>Local fixture</title><label>Fixture contact <input></label><button type="button">Fixture Submit</button>' });
|
||
}
|
||
return route.abort();
|
||
});
|
||
};
|
||
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' });
|
||
await guard(context);
|
||
const page = await context.newPage();
|
||
await page.clock.install({ time: new Date('2026-10-29T12:00:00Z') }); await page.clock.pauseAt(new Date('2026-10-29T12:00:01Z'));
|
||
await page.goto(origin + target.route, { waitUntil: 'domcontentloaded' });
|
||
const dialog = page.locator('[data-service-inquiry-drawer]');
|
||
const frame = dialog.locator('iframe');
|
||
const before = evidence.jotformAttempts.length;
|
||
assert.equal(await frame.getAttribute('src'), null);
|
||
await page.clock.runFor(3999);
|
||
assert.equal(await dialog.evaluate((d) => d.open), false);
|
||
await page.clock.runFor(100);
|
||
assert.equal(await dialog.evaluate((d) => d.open), true);
|
||
assert.equal(await frame.getAttribute('src'), expectedUrl(target.locale));
|
||
await page.waitForFunction(() => document.querySelector('iframe[data-jotform-iframe-frame]')?.src.includes('consultation'));
|
||
assert.equal(await page.locator('[data-service-inquiry-trigger]').count(), 1);
|
||
const heading = await dialog.locator('h2').textContent();
|
||
assert.ok(target.locale === 'es' ? /Conozca|preguntas/.test(heading) : /Explore|Questions/.test(heading));
|
||
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
|
||
await page.screenshot({ path: path.join(output, target.screenshot + '-' + width + '.png'), fullPage: true });
|
||
evidence.screenshots.push(target.screenshot + '-' + width + '.png');
|
||
await page.keyboard.press('Escape');
|
||
await page.clock.runFor(100);
|
||
assert.equal(await dialog.evaluate((d) => d.open), false);
|
||
const expiry = await page.evaluate((k) => Number(localStorage.getItem(k)), key);
|
||
const now = await page.evaluate(() => Date.now());
|
||
assert.ok(expiry > now && expiry <= now + duration);
|
||
await page.clock.runFor(10000);
|
||
await page.locator('[data-service-inquiry-trigger]').focus();
|
||
await page.keyboard.press('Enter');
|
||
await page.clock.runFor(100);
|
||
assert.equal(await dialog.evaluate((d) => d.open), true);
|
||
assert.equal(await frame.count(), 1);
|
||
await page.locator('[data-service-inquiry-close]').click();
|
||
await page.clock.runFor(100);
|
||
assert.equal(await page.evaluate(() => document.activeElement?.hasAttribute('data-service-inquiry-trigger')), true);
|
||
const renewed = await page.evaluate((k) => Number(localStorage.getItem(k)), key);
|
||
assert.ok(renewed > expiry);
|
||
assert.ok(evidence.jotformAttempts.length <= before + 1);
|
||
evidence.checks++;
|
||
await context.close();
|
||
}
|
||
}
|
||
// Scroll wins, suppression crosses locale/service navigation, manual opening bypasses it.
|
||
const context = await browser.newContext({ serviceWorkers: 'block', reducedMotion: 'reduce' });
|
||
await guard(context);
|
||
const page = await context.newPage(); await page.clock.install({ time: new Date('2026-10-29T12:00:00Z') }); await page.clock.pauseAt(new Date('2026-10-29T12:00:01Z'));
|
||
await page.goto(origin + '/aba-therapy', { waitUntil: 'domcontentloaded' });
|
||
await page.evaluate(() => scrollTo({ top: (document.documentElement.scrollHeight - innerHeight) * .6, behavior: 'instant' }));
|
||
await page.clock.runFor(100);
|
||
assert.equal(await page.locator('dialog').evaluate((d) => d.open), true);
|
||
await page.keyboard.press('Escape'); await page.clock.runFor(100);
|
||
await page.goto(origin + '/es/learner-social-club', { waitUntil: 'domcontentloaded' });
|
||
await page.clock.runFor(5000);
|
||
await page.evaluate(() => scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' })); await page.clock.runFor(100);
|
||
assert.equal(await page.locator('dialog').evaluate((d) => d.open), false);
|
||
assert.equal(await page.locator('dialog iframe').getAttribute('src'), null);
|
||
await page.locator('[data-service-inquiry-trigger]').click(); await page.clock.runFor(100);
|
||
assert.equal(await page.locator('dialog').evaluate((d) => d.open), true);
|
||
await page.keyboard.press('Escape'); await page.clock.runFor(100);
|
||
// A second page-load initialization disposes previous listeners without duplicating activation.
|
||
await page.evaluate(() => document.dispatchEvent(new Event('astro:page-load')));
|
||
await page.locator('[data-service-inquiry-trigger]').click(); await page.clock.runFor(100);
|
||
assert.equal(await page.locator('dialog iframe').count(), 1);
|
||
evidence.checks += 2;
|
||
await context.close();
|
||
|
||
for (const preference of ['expired', 'invalid', 'denied', 'editing', 'short', 'other-modal']) {
|
||
const ctx = await browser.newContext({ serviceWorkers: 'block', reducedMotion: 'reduce' }); await guard(ctx);
|
||
await ctx.addInitScript(({ preference, key }) => {
|
||
if (preference === 'expired') localStorage.setItem(key, '1');
|
||
if (preference === 'invalid') localStorage.setItem(key, 'broken');
|
||
if (preference === 'denied') Object.defineProperty(window, 'localStorage', { get() { throw new Error('Denied'); } });
|
||
}, { preference, key });
|
||
const p = await ctx.newPage(); await p.clock.install({ time: new Date('2026-10-29T12:00:00Z') }); await p.clock.pauseAt(new Date('2026-10-29T12:00:01Z')); await p.goto(origin + '/aba-therapy', { waitUntil: 'domcontentloaded' });
|
||
if (preference === 'editing') await p.evaluate(() => { const field = document.createElement('input'); document.body.prepend(field); field.focus(); });
|
||
if (preference === 'other-modal') await p.evaluate(() => { const d = document.createElement('dialog'); document.body.append(d); d.showModal(); });
|
||
if (preference === 'short') {
|
||
await p.evaluate(() => { document.querySelectorAll('main > :not([data-service-inquiry-drawer]), header, footer').forEach((e) => e instanceof HTMLElement && (e.style.display = 'none')); });
|
||
await p.clock.runFor(100);
|
||
assert.equal(await p.locator('[data-service-inquiry-drawer]').evaluate((d) => d.open), false);
|
||
}
|
||
await p.clock.runFor(4100);
|
||
assert.equal(await p.locator('[data-service-inquiry-drawer]').evaluate((d) => d.open), !['editing', 'other-modal'].includes(preference), preference);
|
||
evidence.checks++; await ctx.close();
|
||
}
|
||
const backContext = await browser.newContext({ serviceWorkers: 'block', reducedMotion: 'reduce' });
|
||
await guard(backContext);
|
||
const backPage = await backContext.newPage();
|
||
await backPage.goto(origin + '/aba-therapy', { waitUntil: 'domcontentloaded' });
|
||
await backPage.evaluate(() => {
|
||
window.dispatchEvent(new PageTransitionEvent('pagehide', { persisted: true }));
|
||
window.dispatchEvent(new PageTransitionEvent('pageshow', { persisted: true }));
|
||
});
|
||
assert.equal(await backPage.evaluate((storageKey) => localStorage.getItem(storageKey), key), null);
|
||
await backPage.locator('[data-service-inquiry-trigger]').click();
|
||
assert.equal(await backPage.locator('dialog iframe').getAttribute('src'), expectedUrl('en'));
|
||
evidence.checks++;
|
||
await backContext.close();
|
||
}
|
||
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']) {
|
||
const ctx = await browser.newContext({ serviceWorkers: 'block', viewport: { width: locale === 'es' ? 390 : 1440, height: 900 } });
|
||
await guard(ctx);
|
||
await ctx.addInitScript((key) => localStorage.setItem(key, String(Date.now() + 86400000)), key);
|
||
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 + (locale === 'es' ? '/es/aba-therapy' : '/aba-therapy'));
|
||
const d = p.locator('[data-service-inquiry-drawer]');
|
||
const ack = d.locator('[data-service-inquiry-completed]');
|
||
const trigger = p.locator('[data-service-inquiry-trigger]');
|
||
const send = async (changes = {}) => p.evaluate((changes) => {
|
||
const frame = document.querySelector('dialog iframe');
|
||
const source = changes.wrongSource ? window : frame.contentWindow;
|
||
dispatchEvent(new MessageEvent('message', { origin: changes.origin ?? 'https://form.jotform.com', source,
|
||
data: { action: 'submission-completed', formID: '262946078314059', ...changes.data } }));
|
||
}, changes);
|
||
await send(); // Closed and unactivated.
|
||
assert.equal(await d.getAttribute('data-submission-completed'), null);
|
||
await trigger.click(); await p.clock.runFor(600);
|
||
// CSS transition rendering uses browser time, independently of mocked JS timers.
|
||
await delay(550);
|
||
await p.waitForFunction(() => [...document.querySelectorAll('iframe')].some(f => f.src.includes('consultation')));
|
||
for (const wrong of [{ origin: 'https://form.jotform.com.evil.invalid' }, { wrongSource: true },
|
||
{ data: { formID: 'other' } }, { data: { action: 'setHeight' } }]) await send(wrong);
|
||
await p.clock.runFor(6000);
|
||
assert.equal(await d.evaluate(d => d.open), true);
|
||
assert.equal(await ack.isVisible(), false);
|
||
evidence.checks++;
|
||
const child = p.frames().find(f => f.url() === expectedUrl(locale));
|
||
assert.ok(child, 'authorized fixture frame loaded');
|
||
await child.evaluate(() => parent.postMessage({ action: 'submission-completed', formID: '262946078314059' }, '*'));
|
||
await p.waitForFunction(() => document.querySelector('dialog')?.dataset.submissionCompleted === 'true');
|
||
assert.equal(await ack.isVisible(), true);
|
||
assert.match(await ack.textContent(), locale === 'es' ? /¡Gracias!/ : /Thank you!/);
|
||
const expiry = await p.evaluate(key => Number(localStorage.getItem(key)), key);
|
||
const now = await p.evaluate(() => Date.now());
|
||
assert.equal(expiry, now + duration);
|
||
await p.clock.runFor(3000); await send(); // Duplicate must not restart countdown.
|
||
await p.clock.runFor(1999);
|
||
assert.equal(await d.evaluate(d => d.open && d.classList.contains('is-open')), true);
|
||
const acknowledgmentBox = await ack.boundingBox();
|
||
assert.ok(acknowledgmentBox && acknowledgmentBox.x >= 0 && acknowledgmentBox.y >= 0 &&
|
||
acknowledgmentBox.y + acknowledgmentBox.height <= 900, 'acknowledgment is inside the visible viewport');
|
||
await p.screenshot({ path: path.join(output, `completion-${locale}.png`) });
|
||
evidence.screenshots.push(`completion-${locale}.png`);
|
||
await p.clock.runFor(1);
|
||
assert.equal(await d.evaluate(d => d.open && !d.classList.contains('is-open')), true, 'slide-out starts after full 5 seconds');
|
||
await p.clock.runFor(500);
|
||
assert.equal(await d.evaluate(d => d.open), false);
|
||
assert.equal(await trigger.evaluate(e => e === document.activeElement), true);
|
||
assert.equal(await p.evaluate(key => Number(localStorage.getItem(key)), key), expiry);
|
||
evidence.checks++;
|
||
await p.evaluate(() => window.oldFixtureWindow = document.querySelector('dialog iframe').contentWindow);
|
||
const count = evidence.jotformAttempts.length;
|
||
await trigger.click(); await p.clock.runFor(600);
|
||
assert.equal(await ack.isVisible(), false);
|
||
assert.equal(await d.locator('iframe').count(), 1);
|
||
assert.equal(await p.evaluate(() => window.oldFixtureWindow === document.querySelector('dialog iframe').contentWindow), false);
|
||
await p.waitForFunction(() => document.querySelector('[data-jotform-form-a]')?.dataset.jotformDocumentLoaded === 'true');
|
||
assert.equal(evidence.jotformAttempts.length, count + 1);
|
||
await p.evaluate(() => dispatchEvent(new MessageEvent('message', { origin: 'https://form.jotform.com', source: window.oldFixtureWindow,
|
||
data: { action: 'submission-completed', formID: '262946078314059' } })));
|
||
assert.equal(await ack.isVisible(), false);
|
||
await send(); await p.clock.runFor(1000);
|
||
await p.keyboard.press('Escape'); await p.clock.runFor(500);
|
||
assert.equal(await d.evaluate(d => d.open), false);
|
||
await trigger.click(); await p.clock.runFor(5000);
|
||
assert.equal(await d.evaluate(d => d.open), true, 'manual close canceled prior countdown');
|
||
assert.equal(await ack.isVisible(), false);
|
||
evidence.checks++;
|
||
await send();
|
||
await p.evaluate(() => document.dispatchEvent(new Event('astro:before-swap')));
|
||
await p.clock.runFor(6000);
|
||
assert.equal(await d.evaluate(d => d.open), false);
|
||
await p.evaluate(() => document.dispatchEvent(new Event('astro:page-load')));
|
||
await trigger.click(); await p.clock.runFor(6000);
|
||
assert.equal(await ack.isVisible(), false);
|
||
assert.equal(await d.evaluate(d => d.open), true);
|
||
evidence.checks++;
|
||
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);
|
||
const p = await ctx.newPage(); await p.goto(origin + '/employee-portal');
|
||
assert.equal(await p.locator('meta[name="robots"]').getAttribute('content'), 'noindex,nofollow');
|
||
assert.equal(await p.locator('[data-portal-content]').isVisible(), false);
|
||
const input = p.locator('[data-portal-code]');
|
||
await input.fill('synthetic-incorrect'); await input.press('Enter');
|
||
await p.waitForFunction(() => document.querySelector('[data-portal-feedback]')?.textContent === 'Invalid Code');
|
||
assert.equal(await p.locator('[data-portal-content]').textContent(), '');
|
||
await input.fill(staffCode); await input.press('Enter');
|
||
await p.locator('[data-portal-content]').waitFor({ state: 'visible' });
|
||
assert.match(await p.locator('[data-portal-content]').textContent(), /Synthetic Staff/);
|
||
assert.doesNotMatch(await p.locator('[data-portal-content]').textContent(), /Synthetic Management|Management fixture only/);
|
||
assert.equal(await p.evaluate(() => window.portalUnsafe), undefined);
|
||
assert.equal(await p.locator('[data-portal-content] img, [data-portal-content] script').count(), 0);
|
||
assert.equal(await p.evaluate(() => document.activeElement?.hasAttribute('data-portal-content')), true);
|
||
assert.equal(await input.inputValue(), '');
|
||
const checkPresentation = async (staff = false) => {
|
||
const content = p.locator('[data-portal-content]');
|
||
assert.equal(await content.locator('.portal-resource-button').count(), staff ? 8 : 4);
|
||
assert.equal(await content.locator('[style], [onclick], .arbitrary-legacy-class, .unrecognized-report-legacy, iframe, form, input').count(), 0);
|
||
assert.equal(await content.locator('a[href^="javascript:"]').count(), 0);
|
||
assert.equal(await content.locator('th[scope="col"]').count(), 2);
|
||
const button = content.locator('.portal-resource-button').first();
|
||
assert.equal(await button.evaluate(e => getComputedStyle(e).backgroundColor), 'rgb(255, 189, 89)');
|
||
const legacyReport = content.getByRole('link', { name: 'Operations Monthly Lineup Report - PM' });
|
||
assert.equal(await legacyReport.getAttribute('class'), 'portal-resource-button');
|
||
assert.equal(await legacyReport.evaluate(e => getComputedStyle(e).backgroundColor), 'rgb(255, 189, 89)');
|
||
assert.equal(await content.locator('li').first().evaluate(e => getComputedStyle(e, '::marker').color), 'rgb(255, 189, 89)');
|
||
assert.equal(await content.locator('h2').first().evaluate(e => getComputedStyle(e).color), 'rgb(37, 64, 128)');
|
||
assert.ok(await content.locator('h2').first().evaluate(e => parseFloat(getComputedStyle(e).fontSize) >= 28));
|
||
assert.equal(await content.locator('.portal-contact-link').first().evaluate(e => getComputedStyle(e).color), 'rgb(169, 70, 0)');
|
||
await button.focus();
|
||
assert.equal(await button.evaluate(e => getComputedStyle(e).outlineStyle), 'solid');
|
||
assert.equal(await button.evaluate(e => getComputedStyle(e).outlineWidth), '3px');
|
||
};
|
||
await checkPresentation(true);
|
||
const staffContent = p.locator('[data-portal-content]');
|
||
const grid = staffContent.locator('ul.portal-template-grid');
|
||
assert.equal(await grid.count(), 1);
|
||
assert.equal(await grid.locator('li').count(), 3);
|
||
assert.equal(await grid.locator('a.portal-resource-button').count(), 3);
|
||
const columns = await grid.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(/\s+/).length);
|
||
assert.equal(columns, width >= 768 ? 2 : 1, 'staff templates follow responsive column count');
|
||
assert.equal(await staffContent.getByRole('link', { name: 'General staff resource' }).getAttribute('class'), 'portal-resource-button');
|
||
assert.equal(await staffContent.getByRole('link', { name: 'Staff email' }).getAttribute('class'), 'portal-contact-link');
|
||
assert.equal(await p.evaluate(async (url) => (await fetch(url)).status, origin + staffResourcePath), 401);
|
||
await p.evaluate(() => {
|
||
window.portalObjectUrlRevocations = [];
|
||
const revoke = URL.revokeObjectURL.bind(URL);
|
||
URL.revokeObjectURL = (url) => { window.portalObjectUrlRevocations.push(url); revoke(url); };
|
||
});
|
||
const staffLink = staffContent.getByRole('link', { name: "Staff's Safety & Training: Résumé (2026)!" });
|
||
const staffDownload = p.waitForEvent('download');
|
||
await staffLink.click();
|
||
assert.equal((await staffDownload).suggestedFilename(), 'Staffs-Safety-and-Training-Resume-2026.pdf');
|
||
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent === 'Downloaded Staffs-Safety-and-Training-Resume-2026.pdf.');
|
||
assert.equal(await p.evaluate(() => window.portalObjectUrlRevocations.length), 0, 'Blob URL remains valid while the browser begins the download');
|
||
assert.equal(p.url(), origin + '/employee-portal');
|
||
const beforeCrossRole = evidence.resourceRequests.length;
|
||
await staffLink.evaluate((link, href) => { link.href = href; }, managementResourcePath);
|
||
await staffLink.click();
|
||
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent?.includes('not available from this portal'));
|
||
assert.equal(evidence.resourceRequests.length, beforeCrossRole, 'staff credential is not sent to management resources');
|
||
assert.deepEqual(await p.evaluate(() => ({ local: { ...localStorage }, session: { ...sessionStorage } })), { local: {}, session: {} });
|
||
assert.equal(p.url().includes(staffCode), false);
|
||
assert.ok(await p.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
|
||
await p.screenshot({ path: path.join(output, 'portal-synthetic-' + width + '.png'), fullPage: true });
|
||
evidence.screenshots.push('portal-synthetic-' + width + '.png');
|
||
await p.reload(); assert.equal(await p.locator('[data-portal-content]').isVisible(), false);
|
||
await input.fill(managementCode); await input.press('Enter');
|
||
await p.locator('[data-portal-content]').waitFor({ state: 'visible' });
|
||
assert.match(await p.locator('[data-portal-content]').textContent(), /Synthetic Management/);
|
||
assert.doesNotMatch(await p.locator('[data-portal-content]').textContent(), /Staff fixture/);
|
||
await checkPresentation();
|
||
assert.equal(await p.evaluate(async (url) => (await fetch(url)).status, origin + managementResourcePath), 401);
|
||
await p.evaluate(() => {
|
||
window.portalObjectUrlRevocations = [];
|
||
const revoke = URL.revokeObjectURL.bind(URL);
|
||
URL.revokeObjectURL = (url) => { window.portalObjectUrlRevocations.push(url); revoke(url); };
|
||
});
|
||
const managementLink = p.getByRole('link', { name: 'Management – Policies / Q&A #1' });
|
||
const managementDownload = p.waitForEvent('download');
|
||
await managementLink.click();
|
||
assert.equal((await managementDownload).suggestedFilename(), 'Management-Policies-Q-and-A-1.docx');
|
||
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent === 'Downloaded Management-Policies-Q-and-A-1.docx.');
|
||
assert.equal(await p.evaluate(() => window.portalObjectUrlRevocations.length), 0, 'Blob URL remains valid while the browser begins the download');
|
||
const beforeManagementCrossRole = evidence.resourceRequests.length;
|
||
await managementLink.evaluate((link, href) => { link.href = href; }, staffResourcePath);
|
||
await managementLink.click();
|
||
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent?.includes('not available from this portal'));
|
||
assert.equal(evidence.resourceRequests.length, beforeManagementCrossRole, 'management credential is not sent to staff resources');
|
||
assert.equal(p.url().includes(managementCode), false);
|
||
assert.equal(await p.locator('[data-portal-content] ul.portal-template-grid').count(), 0, 'management view retains original layout');
|
||
await p.screenshot({ path: path.join(output, 'portal-management-synthetic-' + width + '.png'), fullPage: true });
|
||
evidence.screenshots.push('portal-management-synthetic-' + width + '.png');
|
||
evidence.checks++; await ctx.close();
|
||
}
|
||
for (const js of [false, true]) {
|
||
const ctx = await browser.newContext({ javaScriptEnabled: js, serviceWorkers: 'block' }); await guard(ctx);
|
||
const p = await ctx.newPage(); await p.goto(origin + '/employee-portal');
|
||
assert.equal(await p.locator('[data-portal-code]').isDisabled(), true);
|
||
assert.equal(await p.locator('[data-portal-content]').isVisible(), false);
|
||
if (!js) assert.ok(await p.locator('noscript').last().isVisible());
|
||
evidence.checks++; await ctx.close();
|
||
}
|
||
assert.equal(evidence.blocked.filter((r) => !['GET','HEAD'].includes(r.method)).length, 0);
|
||
evidence.status = 'passed';
|
||
} catch (error) {
|
||
evidence.error = error instanceof Error ? error.message : String(error);
|
||
throw error;
|
||
} finally {
|
||
await fs.writeFile(path.join(output, 'evidence.json'), JSON.stringify(evidence, null, 2));
|
||
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');
|
||
}
|
||
await browser?.close();
|
||
console.log(JSON.stringify({ ...evidence, artifactDirectory: output }, null, 2));
|
||
}
|