chrome-devtools-frontend
Version:
Chrome DevTools UI
287 lines (256 loc) • 14.4 kB
text/typescript
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
import {TraceLoader} from '../../../testing/TraceLoader.js';
import * as Trace from '../trace.js';
function countMetricOcurrences(
scoresByMetricName: Array<
Map<Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName,
Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricScore>>,
metricName: Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName): number {
return scoresByMetricName.reduce((acc, val) => {
if (val.get(metricName)) {
return acc + 1;
}
return acc;
}, 0);
}
describeWithEnvironment('PageLoadMetricsHandler', function() {
describe('contentful paints', () => {
it('obtains all the FCP and LCP events for all frames', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'multiple-navigations-with-iframes.json.gz');
const {Meta, PageLoadMetrics} = parsedTrace;
const {mainFrameId} = Meta;
const pageLoadMetricsData = PageLoadMetrics.metricScoresByFrameId;
assert.strictEqual(pageLoadMetricsData.size, 3);
const pageLoadEventsForMainFrame = pageLoadMetricsData.get(mainFrameId);
if (!pageLoadEventsForMainFrame) {
assert.fail('Page load events for main frame were unexpectedly null.');
}
// There are 2 FCP events and 2 LCP events on the main frame: one for the first navigation,
// and one for the second.
assert.strictEqual(pageLoadEventsForMainFrame.size, 2);
const scoresByMetricName = [...pageLoadEventsForMainFrame.values()];
const fcpCount =
countMetricOcurrences(scoresByMetricName, Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName.FCP);
const lcpCount =
countMetricOcurrences(scoresByMetricName, Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName.LCP);
assert.strictEqual(fcpCount, 2);
assert.strictEqual(lcpCount, 2);
});
it('finds the right FCP and LCP events for a trace for a page that was refreshed', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'reload-and-trace-page.json.gz');
const {Meta, PageLoadMetrics} = parsedTrace;
const {mainFrameId} = Meta;
const pageLoadMetricsData = PageLoadMetrics.metricScoresByFrameId;
// Only one frame to deal with
assert.strictEqual(pageLoadMetricsData.size, 1);
const pageLoadEventsForMainFrame = pageLoadMetricsData.get(mainFrameId);
if (!pageLoadEventsForMainFrame) {
assert.fail('Page load events for main frame were unexpectedly null.');
}
// Single FCP event that occurred after the refresh.
assert.strictEqual(pageLoadEventsForMainFrame.size, 1);
const scoresByMetricName = [...pageLoadEventsForMainFrame.values()];
const fcpCount =
countMetricOcurrences(scoresByMetricName, Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName.FCP);
const lcpCount =
countMetricOcurrences(scoresByMetricName, Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName.LCP);
assert.strictEqual(fcpCount, 1);
assert.strictEqual(lcpCount, 1);
});
it('stores the navigation event as part of the metric', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'reload-and-trace-page.json.gz');
const {Meta, PageLoadMetrics} = parsedTrace;
const {mainFrameId, navigationsByFrameId} = Meta;
const navigationBeforeMetrics = navigationsByFrameId.get(mainFrameId)?.[0];
const navigationId = navigationBeforeMetrics?.args.data?.navigationId;
if (!navigationBeforeMetrics || !navigationId) {
assert.fail('Could not find expected navigation event or its navigation ID');
}
const pageLoadMetricsData = PageLoadMetrics.metricScoresByFrameId;
// Only one frame to deal with
assert.strictEqual(pageLoadMetricsData.size, 1);
const pageLoadEventsForMainFrame = pageLoadMetricsData.get(mainFrameId);
if (!pageLoadEventsForMainFrame) {
assert.fail('Page load events for main frame were unexpectedly null.');
}
// Single FCP event that occurred after the refresh.
assert.strictEqual(pageLoadEventsForMainFrame.size, 1);
const events = pageLoadEventsForMainFrame.get(navigationId);
const allFoundMetricScoresForMainFrame = events ? Array.from(events.values()) : [];
for (const score of allFoundMetricScoresForMainFrame) {
assert.strictEqual(score.navigation, navigationBeforeMetrics);
}
});
});
describe('markDOMContent frame', () => {
it('obtains them and assigns them to the correct frames', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'multiple-navigations-with-iframes.json.gz');
const {Meta, PageLoadMetrics} = parsedTrace;
const {mainFrameId} = Meta;
const pageLoadMetricsData = PageLoadMetrics.metricScoresByFrameId;
// We expect 3 frames: main frame, and two iframes.
assert.strictEqual(pageLoadMetricsData.size, 3);
const pageLoadEventsForMainFrame = pageLoadMetricsData.get(mainFrameId);
if (!pageLoadEventsForMainFrame) {
assert.fail('Page load events for main frame were unexpectedly null.');
}
// There are 2 MarkDOMContent events on the main frame: one for the first navigation,
// and one for the second.
assert.strictEqual(pageLoadEventsForMainFrame.size, 2);
const dclCount = countMetricOcurrences(
[...pageLoadEventsForMainFrame.values()], Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName.DCL);
assert.strictEqual(dclCount, 2);
});
});
describe('metric scores', () => {
let allMetricScores: Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricScore[];
function getMetricsByName(name: Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName) {
return allMetricScores.filter(metric => metric.metricName === name);
}
function assertMetricNavigationId(
metric: Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricScore, navigationId: string) {
assert.strictEqual(metric.navigation?.args.data?.navigationId, navigationId);
}
const firstNavigationId = '05059ACF683224E6FC7E344F544A4050';
const secondNavigationId = '550FC08C662EF691E1535F305CBC0FCA';
beforeEach(async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'multiple-navigations-with-iframes.json.gz');
const {Meta, PageLoadMetrics} = parsedTrace;
const pageLoadMetricsData = PageLoadMetrics.metricScoresByFrameId.get(Meta.mainFrameId);
if (!pageLoadMetricsData) {
assert.fail('Page load events for main frame were unexpectedly undefined.');
}
const scoresByMetricName = [...pageLoadMetricsData.values()];
allMetricScores = scoresByMetricName.flatMap(metricScores => [...metricScores.values()]);
});
it('extracts DOMContentLoaded correctly', () => {
const domContentLoadedMetrics = getMetricsByName(Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName.DCL);
assert.strictEqual(domContentLoadedMetrics[0].timing, 34520);
assert.strictEqual(
domContentLoadedMetrics[0].classification,
Trace.Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.UNCLASSIFIED);
assertMetricNavigationId(domContentLoadedMetrics[0], firstNavigationId);
assert.strictEqual(domContentLoadedMetrics[1].timing, 40401);
assert.strictEqual(
domContentLoadedMetrics[1].classification,
Trace.Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.UNCLASSIFIED);
assertMetricNavigationId(domContentLoadedMetrics[1], secondNavigationId);
});
it('extracts First Contentful Paint correctly', () => {
const firstContentfulPaints = getMetricsByName(Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName.FCP);
assert.strictEqual(firstContentfulPaints[0].timing, 37269);
assert.strictEqual(
firstContentfulPaints[0].classification,
Trace.Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.GOOD);
assertMetricNavigationId(firstContentfulPaints[0], firstNavigationId);
assert.strictEqual(firstContentfulPaints[1].timing, 42390);
assert.strictEqual(
firstContentfulPaints[1].classification,
Trace.Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.GOOD);
assertMetricNavigationId(firstContentfulPaints[1], secondNavigationId);
});
it('extracts Largest Contentful Paint correctly', () => {
const firstContentfulPaints = getMetricsByName(Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName.LCP);
assert.strictEqual(firstContentfulPaints[0].timing, 37271);
assert.strictEqual(
firstContentfulPaints[0].classification,
Trace.Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.GOOD);
assertMetricNavigationId(firstContentfulPaints[0], firstNavigationId);
assert.strictEqual(firstContentfulPaints[1].timing, 42391);
assert.strictEqual(
firstContentfulPaints[1].classification,
Trace.Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.GOOD);
assertMetricNavigationId(firstContentfulPaints[1], secondNavigationId);
});
it('extracts First Paint correctly', () => {
const firstContentfulPaints = getMetricsByName(Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName.FP);
assert.strictEqual(firstContentfulPaints[0].timing, 37269);
assert.strictEqual(
firstContentfulPaints[0].classification,
Trace.Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.UNCLASSIFIED);
assertMetricNavigationId(firstContentfulPaints[0], firstNavigationId);
assert.strictEqual(firstContentfulPaints[1].timing, 42389);
assert.strictEqual(
firstContentfulPaints[1].classification,
Trace.Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.UNCLASSIFIED);
assertMetricNavigationId(firstContentfulPaints[1], secondNavigationId);
});
it('extracts Load correctly', () => {
const firstContentfulPaints = getMetricsByName(Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName.L);
assert.strictEqual(firstContentfulPaints[0].timing, 148980);
assert.strictEqual(
firstContentfulPaints[0].classification,
Trace.Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.UNCLASSIFIED);
assertMetricNavigationId(firstContentfulPaints[0], firstNavigationId);
assert.strictEqual(firstContentfulPaints[1].timing, 161333);
assert.strictEqual(
firstContentfulPaints[1].classification,
Trace.Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.UNCLASSIFIED);
assertMetricNavigationId(firstContentfulPaints[1], secondNavigationId);
});
it('provides metric scores sorted in ASC order by their events\' timestamps', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'multiple-navigations-with-iframes.json.gz');
const {Meta, PageLoadMetrics} = parsedTrace;
const pageLoadMetricsData = PageLoadMetrics.metricScoresByFrameId.get(Meta.mainFrameId);
if (!pageLoadMetricsData) {
assert.fail('Page load events for main frame were unexpectedly null.');
}
const scoresByMetricName = [...pageLoadMetricsData.values()];
const flatResults = scoresByMetricName.map(metricScores => [...metricScores.values()])
.reduce((acc, metricScore) => acc.concat(metricScore), []);
const timestamps = [];
for (const metricScore of flatResults) {
if (metricScore.event) {
timestamps.push(metricScore.event.ts);
}
}
let previousTimestamp = timestamps[0];
for (let i = 1; i < timestamps.length; i++) {
assert.isAtLeast(timestamps[i], previousTimestamp);
previousTimestamp = timestamps[i];
}
});
});
describe('FLEDGE fenced frames', () => {
it('is able to parse a trace containing fenced frames without erroring', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'fenced-frame-fledge.json.gz');
const {PageLoadMetrics} = parsedTrace;
assert.strictEqual(PageLoadMetrics.metricScoresByFrameId.size, 3);
});
});
describe('Marker events', () => {
let mainFrameId: string;
let allMarkerEvents: Trace.Types.Events.PageLoadEvent[];
beforeEach(async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'multiple-navigations-with-iframes.json.gz');
const {PageLoadMetrics, Meta} = parsedTrace;
mainFrameId = Meta.mainFrameId;
allMarkerEvents = PageLoadMetrics.allMarkerEvents;
});
it('extracts all marker events from a trace correctly', () => {
for (const metricName of Trace.Types.Events.MarkerName) {
const markerEventsOfThisType = allMarkerEvents.filter(event => event.name === metricName);
// There should be 2 events for each marker and all of them should correspond to the main frame
assert.lengthOf(markerEventsOfThisType, 2);
assert.isTrue(markerEventsOfThisType.every(
marker => Trace.Handlers.ModelHandlers.PageLoadMetrics.getFrameIdForPageLoadEvent(marker) === mainFrameId));
}
});
it('only marker events are exported in allMarkerEvents', () => {
for (const marker of allMarkerEvents) {
assert.isTrue(Trace.Types.Events.isMarkerEvent(marker));
}
});
it('only stores the largest contentful paint with the highest candidate index', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'multiple-lcp-main-frame.json.gz');
const {PageLoadMetrics} = parsedTrace;
const pageLoadMarkers = PageLoadMetrics.allMarkerEvents;
const largestContentfulPaints = pageLoadMarkers.filter(Trace.Types.Events.isLargestContentfulPaintCandidate);
assert.lengthOf(largestContentfulPaints, 1);
assert.strictEqual(largestContentfulPaints[0].args.data?.candidateIndex, 2);
});
});
});