77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
/**
|
|
* The chunk-recovery guard.
|
|
*
|
|
* The messages below are the real ones each engine throws — the detection is a
|
|
* string match against three vendors who agree on nothing, so a fixture copied
|
|
* from the wrong browser would pass while production kept failing.
|
|
*/
|
|
import assert from 'node:assert/strict';
|
|
import { test } from 'node:test';
|
|
import { canRecoverByReloading, isStaleChunkError } from './staleChunk';
|
|
|
|
/** `sessionStorage` and `location` do not exist under `node:test`. */
|
|
const store = new Map<string, string>();
|
|
(globalThis as unknown as { window: unknown }).window = {
|
|
sessionStorage: {
|
|
getItem: (k: string) => store.get(k) ?? null,
|
|
setItem: (k: string, v: string) => void store.set(k, v),
|
|
},
|
|
location: { reload: () => {} },
|
|
};
|
|
|
|
test('recognises what each browser actually throws', () => {
|
|
// Chrome and Edge — the message from the reported failure.
|
|
assert.ok(
|
|
isStaleChunkError(
|
|
new Error(
|
|
'Failed to fetch dynamically imported module: https://app.nearledaily.com/assets/InventoryPage-BAzj-ICF.js',
|
|
),
|
|
),
|
|
);
|
|
// Firefox.
|
|
assert.ok(isStaleChunkError(new Error('error loading dynamically imported module: /assets/x.js')));
|
|
// Safari.
|
|
assert.ok(isStaleChunkError(new Error('Importing a module script failed.')));
|
|
// Vite's own preload helper, for a split chunk's stylesheet.
|
|
assert.ok(isStaleChunkError(new Error('Unable to preload CSS for /assets/x.css')));
|
|
});
|
|
|
|
test('does not mistake a real bug for a missing chunk', () => {
|
|
// The distinction that matters: reloading past one of these would hide a
|
|
// genuine crash behind an endless refresh.
|
|
assert.equal(isStaleChunkError(new TypeError("Cannot read properties of undefined (reading 'map')")), false);
|
|
assert.equal(isStaleChunkError(new Error('Request failed (HTTP 500)')), false);
|
|
assert.equal(isStaleChunkError(null), false);
|
|
assert.equal(isStaleChunkError(undefined), false);
|
|
});
|
|
|
|
test('reloads once, then refuses so it cannot loop', () => {
|
|
store.clear();
|
|
const error = new Error('Failed to fetch dynamically imported module: /assets/a.js');
|
|
|
|
assert.equal(canRecoverByReloading(error), true, 'first failure should reload');
|
|
|
|
// Stamp a reload as having just happened.
|
|
store.set('nearle:stale-chunk-reload-at', String(Date.now()));
|
|
assert.equal(
|
|
canRecoverByReloading(error),
|
|
false,
|
|
'a second failure inside the cooldown must surface, not reload again',
|
|
);
|
|
});
|
|
|
|
test('recovers again from a later deploy', () => {
|
|
store.clear();
|
|
// A reload well outside the 30s window — a different deploy, hours later.
|
|
store.set('nearle:stale-chunk-reload-at', String(Date.now() - 60 * 60_000));
|
|
assert.equal(
|
|
canRecoverByReloading(new Error('Failed to fetch dynamically imported module: /assets/b.js')),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('never reloads for an error that is not a missing chunk', () => {
|
|
store.clear();
|
|
assert.equal(canRecoverByReloading(new TypeError('x is not a function')), false);
|
|
});
|