test(perf): count the long task that dispatches the J2 click

A long task's startTime precedes the click event's timestamp when the
listener runs inside it, so the start-time filter dropped the task that
performs the interaction. Long tasks now count when their range overlaps
the window: on one main thread only the dispatching task can overlap the
click. Layout shifts keep the start-time filter. J1 is unchanged (its
window starts at -Infinity).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-29 08:58:37 +02:00
1 parent 994740ee94
commit 7de590c16d
3 files changed
+43 -3

No files matched your search

@@ -629,7 +629,12 @@ test('drops performance entries from before the click and keeps recent-input shi
},
]);
longTask.emit([
{ duration: 250, entryType: 'longtask', startTime: beforeClick },
// A buffered J1 task that ended before the click.
{
duration: 250,
entryType: 'longtask',
startTime: beforeClick - 300,
},
{ duration: 90, entryType: 'longtask', startTime: now() },
]);
openSource(fixture);
@@ -642,6 +647,30 @@ test('drops performance entries from before the click and keeps recent-input shi
assert.deepEqual(state.longTaskDurationsMs, [90]);
});
test('counts the long task that dispatches the click although it began before the event', async () => {
const fixture = createOpenSourceFixture();
const [, longTask] = fixture.observers as [FakeObserver, FakeObserver];
const now = () => fixture.window.performance.now();
fixture.card.addEventListener('click', () => openSource(fixture));
fixture.card.click();
const clickMs =
(fixture.rawState().start?.epochMs ?? 0) -
fixture.window.performance.timeOrigin;
longTask.emit([
// Began 20 ms before the click stamp and ran through it: the task
// that dispatched the click and rendered the page.
{ duration: 120, entryType: 'longtask', startTime: clickMs - 20 },
// Ended before the click: earlier work, not part of the journey.
{ duration: 60, entryType: 'longtask', startTime: clickMs - 100 },
{ duration: 70, entryType: 'longtask', startTime: now() },
]);
await settle();
const state = fixture.state();
assert.equal(state.final, true);
assert.equal(state.counters.longTasks, 2);
assert.deepEqual(state.longTaskDurationsMs, [120, 70]);
});
test('rejects a click start whose sentinel could not be sent', async () => {
const fixture = createOpenSourceFixture({ bridge: false });
fixture.card.addEventListener('click', () => openSource(fixture));
@@ -199,6 +199,17 @@ export function journeyRendererProbeScript(
const entryEpochMs = performance.timeOrigin + entry.startTime;
return entryEpochMs >= fromEpochMs && entryEpochMs <= untilEpochMs;
};
// A task overlaps the window when it ends after the start. The main
// thread runs one task at a time, so the only task that overlaps the
// click is the one that dispatches it, which began before the event's
// timestamp and must still count.
const overlapsWindow = (entry: PerformanceEntry, untilEpochMs: number) => {
const entryEpochMs = performance.timeOrigin + entry.startTime;
return (
entryEpochMs + entry.duration >= fromEpochMs &&
entryEpochMs <= untilEpochMs
);
};
const acceptLayoutShift = (
entries: readonly PerformanceEntry[],
@@ -227,7 +238,7 @@ export function journeyRendererProbeScript(
untilEpochMs: number
): void => {
for (const entry of entries) {
if (entry.duration <= 50 || !inWindow(entry, untilEpochMs)) {
if (entry.duration <= 50 || !overlapsWindow(entry, untilEpochMs)) {
continue;
}
state.counters.longTasks += 1;