mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-18 18:33:06 +08:00
fix: report page load timing after load handlers complete
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const ts = require('typescript');
|
||||
|
||||
function fixture({ readyState = 'loading', navigation, legacy } = {}) {
|
||||
const callbacks = new Map();
|
||||
const listeners = new Set();
|
||||
const reports = [];
|
||||
let nextId = 0;
|
||||
const previousOnload = () => {};
|
||||
const window = {
|
||||
onload: previousOnload,
|
||||
performance: {
|
||||
...(navigation === undefined
|
||||
? {}
|
||||
: { getEntriesByType: () => [navigation] }),
|
||||
timing: legacy,
|
||||
},
|
||||
setTimeout(fn) {
|
||||
callbacks.set(++nextId, fn);
|
||||
return nextId;
|
||||
},
|
||||
clearTimeout(id) {
|
||||
callbacks.delete(id);
|
||||
},
|
||||
addEventListener(type, fn, options) {
|
||||
assert.equal(type, 'load');
|
||||
assert.equal(options.once, true);
|
||||
listeners.add(fn);
|
||||
},
|
||||
removeEventListener(type, fn) {
|
||||
assert.equal(type, 'load');
|
||||
listeners.delete(fn);
|
||||
},
|
||||
};
|
||||
const compiled = ts.transpileModule(
|
||||
fs.readFileSync(
|
||||
path.join(__dirname, '../../src/utils/navigationTiming.ts'),
|
||||
'utf8',
|
||||
),
|
||||
{
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
},
|
||||
},
|
||||
).outputText;
|
||||
const module = { exports: {} };
|
||||
new Function('module', 'exports', 'window', 'document', compiled)(
|
||||
module,
|
||||
module.exports,
|
||||
window,
|
||||
{ readyState },
|
||||
);
|
||||
const observe = () =>
|
||||
module.exports.observePageLoadTiming((v) => reports.push(v));
|
||||
return {
|
||||
reports,
|
||||
window,
|
||||
previousOnload,
|
||||
observe,
|
||||
load() {
|
||||
for (const fn of [...listeners]) {
|
||||
listeners.delete(fn);
|
||||
fn();
|
||||
}
|
||||
},
|
||||
flush() {
|
||||
const pending = [...callbacks.values()];
|
||||
callbacks.clear();
|
||||
for (const fn of pending) fn();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const timing = () => ({
|
||||
startTime: 0,
|
||||
responseStart: 10.4,
|
||||
responseEnd: 14,
|
||||
domInteractive: 30,
|
||||
domComplete: 50,
|
||||
loadEventEnd: 70.6,
|
||||
});
|
||||
|
||||
test('loadEventEnd is read after load handlers complete, with real positive elapsed time', () => {
|
||||
const navigation = { ...timing(), loadEventEnd: 0 };
|
||||
const f = fixture({ navigation });
|
||||
f.observe();
|
||||
f.load();
|
||||
assert.equal(f.reports.length, 0);
|
||||
navigation.loadEventEnd = 70.6;
|
||||
f.flush();
|
||||
assert.deepEqual(f.reports, [
|
||||
{ responseTime: 10, domReadyTime: 16, domParseTime: 20, loadTime: 71 },
|
||||
]);
|
||||
f.load();
|
||||
f.flush();
|
||||
assert.equal(f.reports.length, 1);
|
||||
});
|
||||
|
||||
test('late mounts report an already loaded page without replacing another onload handler', () => {
|
||||
const f = fixture({ readyState: 'complete', navigation: timing() });
|
||||
const dispose = f.observe();
|
||||
assert.equal(f.reports.length, 0);
|
||||
f.flush();
|
||||
assert.equal(f.reports[0].loadTime, 71);
|
||||
dispose();
|
||||
assert.equal(f.window.onload, f.previousOnload);
|
||||
});
|
||||
|
||||
test('legacy epoch timings retain their own navigationStart baseline', () => {
|
||||
const f = fixture({
|
||||
readyState: 'complete',
|
||||
legacy: {
|
||||
navigationStart: 1700000000000,
|
||||
responseStart: 1700000000010,
|
||||
responseEnd: 1700000000014,
|
||||
domInteractive: 1700000000030,
|
||||
domComplete: 1700000000050,
|
||||
loadEventEnd: 1700000000071,
|
||||
},
|
||||
});
|
||||
f.observe();
|
||||
f.flush();
|
||||
assert.deepEqual(f.reports[0], {
|
||||
responseTime: 10,
|
||||
domReadyTime: 16,
|
||||
domParseTime: 20,
|
||||
loadTime: 71,
|
||||
});
|
||||
});
|
||||
|
||||
test('navigation entries take precedence without mixing relative and epoch clocks', () => {
|
||||
const f = fixture({
|
||||
readyState: 'complete',
|
||||
navigation: timing(),
|
||||
legacy: { navigationStart: 1700000000000, loadEventEnd: 0 },
|
||||
});
|
||||
f.observe();
|
||||
f.flush();
|
||||
assert.equal(f.reports[0].loadTime, 71);
|
||||
});
|
||||
|
||||
test('incomplete, negative and nonfinite timings are omitted rather than displayed as fabricated zeroes', () => {
|
||||
const f = fixture({
|
||||
readyState: 'complete',
|
||||
navigation: {
|
||||
...timing(),
|
||||
responseStart: NaN,
|
||||
domInteractive: 10,
|
||||
domComplete: Infinity,
|
||||
loadEventEnd: 0,
|
||||
},
|
||||
});
|
||||
f.observe();
|
||||
f.flush();
|
||||
assert.ok(Object.values(f.reports[0]).every((v) => v === undefined));
|
||||
});
|
||||
|
||||
test('unmount removes a pending load listener and cancels a queued report', () => {
|
||||
for (const loaded of [false, true]) {
|
||||
const f = fixture({ navigation: timing() });
|
||||
const dispose = f.observe();
|
||||
if (loaded) f.load();
|
||||
dispose();
|
||||
f.load();
|
||||
f.flush();
|
||||
assert.equal(f.reports.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('strict-effect remounts leave exactly one active reporter', () => {
|
||||
const f = fixture({ readyState: 'complete', navigation: timing() });
|
||||
f.observe()();
|
||||
f.observe();
|
||||
f.flush();
|
||||
assert.equal(f.reports.length, 1);
|
||||
});
|
||||
|
||||
test('missing performance timings produce no report', () => {
|
||||
const f = fixture({ readyState: 'complete' });
|
||||
f.observe();
|
||||
f.flush();
|
||||
assert.equal(f.reports.length, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user