96 lines
4.6 KiB
JavaScript
96 lines
4.6 KiB
JavaScript
import {test} from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {navigateBrandDocument, visitBrowserDocument} from './brand-navigation.mjs';
|
|
const response = status => ({ok: () => status === 200, status: () => status});
|
|
|
|
test('successful documents are fetched once', async () => {
|
|
let calls = 0;
|
|
await navigateBrandDocument(async () => { calls++; return response(200); }, '/canvas');
|
|
assert.equal(calls, 1);
|
|
});
|
|
test('one proxy failure can recover within the original deadline', async () => {
|
|
const timeouts = []; let elapsed = 0;
|
|
await navigateBrandDocument(async timeout => {
|
|
timeouts.push(timeout); elapsed = 15000;
|
|
return response(timeouts.length === 1 ? 500 : 200);
|
|
}, '/canvas', () => elapsed);
|
|
assert.deepEqual(timeouts, [60000, 45000]);
|
|
});
|
|
test('connection reset and interrupted bootstrap scripts can recover once', async () => {
|
|
for(const failure of ['net::ERR_CONNECTION_RESET','Document bootstrap script request failed: net::ERR_ABORTED']){
|
|
let calls = 0;
|
|
await navigateBrandDocument(async () => {
|
|
if (++calls === 1) throw new Error(failure);
|
|
return response(200);
|
|
}, '/canvas');
|
|
assert.equal(calls, 2);
|
|
}
|
|
});
|
|
test('persistent server failure is not hidden', async () => {
|
|
let calls = 0;
|
|
await assert.rejects(navigateBrandDocument(async () => { calls++; return response(502); }, '/canvas'), /HTTP 502/);
|
|
assert.equal(calls, 2);
|
|
});
|
|
test('missing routes, authentication failures and ordinary timeouts are not retried', async () => {
|
|
for (const status of [401, 403, 404]) {
|
|
let calls = 0;
|
|
await assert.rejects(navigateBrandDocument(async () => { calls++; return response(status); }, '/canvas'), /HTTP/);
|
|
assert.equal(calls, 1);
|
|
}
|
|
let calls = 0;
|
|
await assert.rejects(navigateBrandDocument(async () => { calls++; throw new Error('Timeout 60000ms exceeded'); }, '/canvas'), /Timeout/);
|
|
assert.equal(calls, 1);
|
|
});
|
|
test('recovery cannot extend the total navigation budget', async () => {
|
|
let calls = 0, elapsed = 0;
|
|
await assert.rejects(navigateBrandDocument(async () => {
|
|
calls++; elapsed = 60000; return response(503);
|
|
}, '/canvas', () => elapsed), /budget exhausted/);
|
|
assert.equal(calls, 1);
|
|
});
|
|
|
|
function browserFixture(failBootstrap = false, failHydration = false, blockedOnce = false, readyVisible = true) {
|
|
const listeners = new Map(), calls = [];
|
|
const page = {
|
|
on(event, fn) { if (!listeners.has(event)) listeners.set(event, new Set()); listeners.get(event).add(fn); },
|
|
off(event, fn) { listeners.get(event)?.delete(fn); },
|
|
async goto(url, options) {
|
|
calls.push(options.waitUntil);
|
|
const request = {resourceType: () => 'script', failure: () => ({errorText: 'net::ERR_ABORTED'})};
|
|
for (const fn of listeners.get('request') || []) fn(request);
|
|
if (failBootstrap && calls.length === 1) for (const fn of listeners.get('requestfailed') || []) fn(request);
|
|
return response(200);
|
|
},
|
|
locator() { return {first: () => ({waitFor: async options => { calls.push('hydrated'); if (!readyVisible && options.state !== 'attached') throw Error('Ready marker is hidden by responsive CSS'); if (failHydration || (blockedOnce && calls.length === 2)) throw Error('Hydration timeout'); }})}; },
|
|
};
|
|
return {page, calls, listeners};
|
|
}
|
|
test('document visit completes deferred scripts before checking hydration and removes listeners', async () => {
|
|
const {page, calls, listeners} = browserFixture();
|
|
await visitBrowserDocument(page, '/account');
|
|
assert.deepEqual(calls, ['domcontentloaded', 'hydrated']);
|
|
assert.ok([...listeners.values()].every(set => set.size === 0));
|
|
});
|
|
test('interrupted bootstrap retries only document loading before hydration', async () => {
|
|
const {page, calls} = browserFixture(true, false, true);
|
|
await visitBrowserDocument(page, '/account');
|
|
assert.deepEqual(calls, ['domcontentloaded', 'hydrated', 'domcontentloaded', 'hydrated']);
|
|
});
|
|
test('hydration failure is surfaced without retrying the application', async () => {
|
|
const {page, calls} = browserFixture(false, true);
|
|
await assert.rejects(visitBrowserDocument(page, '/account'), /Hydration timeout/);
|
|
assert.deepEqual(calls, ['domcontentloaded', 'hydrated']);
|
|
});
|
|
|
|
test('cancelled script does not reject an already hydrated document', async () => {
|
|
const {page, calls} = browserFixture(true);
|
|
await visitBrowserDocument(page, '/account');
|
|
assert.deepEqual(calls, ['domcontentloaded', 'hydrated']);
|
|
});
|
|
|
|
test('hydrated pages are ready even when responsive CSS hides the marker', async () => {
|
|
const {page, calls} = browserFixture(false, false, false, false);
|
|
await visitBrowserDocument(page, '/graphic');
|
|
assert.deepEqual(calls, ['domcontentloaded', 'hydrated']);
|
|
});
|