diff --git a/core/audits/layout-shift-elements.js b/core/audits/layout-shift-elements.js index c41a46e7cb72..7498fa65145b 100644 --- a/core/audits/layout-shift-elements.js +++ b/core/audits/layout-shift-elements.js @@ -40,15 +40,28 @@ class LayoutShiftElements extends Audit { * @return {Promise} */ static async audit(artifacts, context) { - const clsElements = artifacts.TraceElements - .filter(element => element.traceEventType === 'layout-shift'); + const {cumulativeLayoutShift: clsSavings, impactByNodeId} = + await CumulativeLayoutShiftComputed.request(artifacts.traces[Audit.DEFAULT_PASS], context); - const clsElementData = clsElements.map(element => { - return { + /** @type {Array<{node: LH.Audit.Details.ItemValue, score?: number}>} */ + const clsElementData = artifacts.TraceElements + .filter(element => element.traceEventType === 'layout-shift') + .map(element => ({ node: Audit.makeNodeItem(element.node), score: element.score, - }; - }); + })); + + if (clsElementData.length < impactByNodeId.size) { + const elementDataImpact = clsElementData.reduce((sum, {score}) => sum += score || 0, 0); + const remainingImpact = Math.max(clsSavings - elementDataImpact, 0); + clsElementData.push({ + node: { + type: 'code', + value: str_(i18n.UIStrings.otherResourceType), + }, + score: remainingImpact, + }); + } /** @type {LH.Audit.Details.Table['headings']} */ const headings = [ @@ -58,15 +71,13 @@ class LayoutShiftElements extends Audit { ]; const details = Audit.makeTableDetails(headings, clsElementData); + let displayValue; - if (clsElementData.length > 0) { + if (impactByNodeId.size > 0) { displayValue = str_(i18n.UIStrings.displayValueElementsFound, - {nodeCount: clsElementData.length}); + {nodeCount: impactByNodeId.size}); } - const {cumulativeLayoutShift: clsSavings} = - await CumulativeLayoutShiftComputed.request(artifacts.traces[Audit.DEFAULT_PASS], context); - const passed = clsSavings <= CumulativeLayoutShift.defaultOptions.p10; return { diff --git a/core/audits/metrics/cumulative-layout-shift.js b/core/audits/metrics/cumulative-layout-shift.js index 1d4532f63bfb..a15d89eae549 100644 --- a/core/audits/metrics/cumulative-layout-shift.js +++ b/core/audits/metrics/cumulative-layout-shift.js @@ -54,7 +54,11 @@ class CumulativeLayoutShift extends Audit { */ static async audit(artifacts, context) { const trace = artifacts.traces[Audit.DEFAULT_PASS]; - const {cumulativeLayoutShift, ...rest} = await ComputedCLS.request(trace, context); + + // impactByNodeId is unused but we don't want it on debug data + // eslint-disable-next-line no-unused-vars + const {cumulativeLayoutShift, impactByNodeId, ...rest} = + await ComputedCLS.request(trace, context); /** @type {LH.Audit.Details.DebugData} */ const details = { diff --git a/core/computed/metrics/cumulative-layout-shift.js b/core/computed/metrics/cumulative-layout-shift.js index d140cf37dcc2..2d70e22bcc4c 100644 --- a/core/computed/metrics/cumulative-layout-shift.js +++ b/core/computed/metrics/cumulative-layout-shift.js @@ -6,6 +6,7 @@ import {makeComputedArtifact} from '../computed-artifact.js'; import {ProcessedTrace} from '../processed-trace.js'; +import * as RectHelpers from '../../lib/rect-helpers.js'; /** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number, impactedNodes?: LH.Artifacts.TraceImpactedNode[]}} LayoutShiftEvent */ @@ -72,6 +73,49 @@ class CumulativeLayoutShift { return layoutShiftEvents; } + /** + * Each layout shift event has a 'score' which is the amount added to the CLS as a result of the given shift(s). + * We calculate the score per element by taking the 'score' of each layout shift event and + * distributing it between all the nodes that were shifted, proportianal to the impact region of + * each shifted element. + * + * @param {LayoutShiftEvent[]} layoutShiftEvents + * @return {Map} + */ + static getImpactByNodeId(layoutShiftEvents) { + /** @type {Map} */ + const impactByNodeId = new Map(); + + for (const event of layoutShiftEvents) { + if (!event.impactedNodes) continue; + + let totalAreaOfImpact = 0; + /** @type {Map} */ + const pixelsMovedPerNode = new Map(); + + for (const node of event.impactedNodes) { + if (!node.node_id || !node.old_rect || !node.new_rect) continue; + + const oldRect = RectHelpers.traceRectToLHRect(node.old_rect); + const newRect = RectHelpers.traceRectToLHRect(node.new_rect); + const areaOfImpact = RectHelpers.getRectArea(oldRect) + + RectHelpers.getRectArea(newRect) - + RectHelpers.getRectOverlapArea(oldRect, newRect); + + pixelsMovedPerNode.set(node.node_id, areaOfImpact); + totalAreaOfImpact += areaOfImpact; + } + + for (const [nodeId, pixelsMoved] of pixelsMovedPerNode.entries()) { + let clsContribution = impactByNodeId.get(nodeId) || 0; + clsContribution += (pixelsMoved / totalAreaOfImpact) * event.weightedScore; + impactByNodeId.set(nodeId, clsContribution); + } + } + + return impactByNodeId; + } + /** * Calculates cumulative layout shifts per cluster (session) of LayoutShift * events -- where a new cluster is created when there's a gap of more than @@ -104,18 +148,20 @@ class CumulativeLayoutShift { /** * @param {LH.Trace} trace * @param {LH.Artifacts.ComputedContext} context - * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}>} + * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, impactByNodeId: Map}>} */ static async compute_(trace, context) { const processedTrace = await ProcessedTrace.request(trace, context); const allFrameShiftEvents = CumulativeLayoutShift.getLayoutShiftEvents(processedTrace); + const impactByNodeId = CumulativeLayoutShift.getImpactByNodeId(allFrameShiftEvents); const mainFrameShiftEvents = allFrameShiftEvents.filter(e => e.isMainFrame); return { cumulativeLayoutShift: CumulativeLayoutShift.calculate(allFrameShiftEvents), cumulativeLayoutShiftMainFrame: CumulativeLayoutShift.calculate(mainFrameShiftEvents), + impactByNodeId, }; } } diff --git a/core/gather/gatherers/trace-elements.js b/core/gather/gatherers/trace-elements.js index 2c4858807fb5..fd86e3aec267 100644 --- a/core/gather/gatherers/trace-elements.js +++ b/core/gather/gatherers/trace-elements.js @@ -15,7 +15,6 @@ import BaseGatherer from '../base-gatherer.js'; import {resolveNodeIdToObjectId} from '../driver/dom.js'; import {pageFunctions} from '../../lib/page-functions.js'; -import * as RectHelpers from '../../lib/rect-helpers.js'; import {Sentry} from '../../lib/sentry.js'; import Trace from './trace.js'; import {ProcessedTrace} from '../../computed/processed-trace.js'; @@ -27,6 +26,8 @@ import {ExecutionContext} from '../driver/execution-context.js'; /** @typedef {{nodeId: number, score?: number, animations?: {name?: string, failureReasonsMask?: number, unsupportedProperties?: string[]}[], type?: string}} TraceElementData */ +const MAX_LAYOUT_SHIFT_ELEMENTS = 15; + /** * @this {HTMLElement} */ @@ -63,75 +64,24 @@ class TraceElements extends BaseGatherer { } /** - * @param {Array} rect - * @return {LH.Artifacts.Rect} - */ - static traceRectToLHRect(rect) { - const rectArgs = { - x: rect[0], - y: rect[1], - width: rect[2], - height: rect[3], - }; - return RectHelpers.addRectTopAndBottom(rectArgs); - } - - /** - * This function finds the top (up to 5) elements that contribute to the CLS score of the page. - * Each layout shift event has a 'score' which is the amount added to the CLS as a result of the given shift(s). - * We calculate the score per element by taking the 'score' of each layout shift event and - * distributing it between all the nodes that were shifted, proportianal to the impact region of - * each shifted element. - * @param {LH.Artifacts.ProcessedTrace} processedTrace - * @return {Array} + * This function finds the top (up to 15) elements that contribute to the CLS score of the page. + * + * @param {LH.Trace} trace + * @param {LH.Gatherer.Context} context + * @return {Promise>} */ - static getTopLayoutShiftElements(processedTrace) { - /** @type {Map} */ - const clsPerNode = new Map(); - const shiftEvents = CumulativeLayoutShift.getLayoutShiftEvents(processedTrace); - - shiftEvents.forEach((event) => { - if (!event || !event.impactedNodes) { - return; - } - - let totalAreaOfImpact = 0; - /** @type {Map} */ - const pixelsMovedPerNode = new Map(); - - event.impactedNodes.forEach(node => { - if (!node.node_id || !node.old_rect || !node.new_rect) { - return; - } - - const oldRect = TraceElements.traceRectToLHRect(node.old_rect); - const newRect = TraceElements.traceRectToLHRect(node.new_rect); - const areaOfImpact = RectHelpers.getRectArea(oldRect) + - RectHelpers.getRectArea(newRect) - - RectHelpers.getRectOverlapArea(oldRect, newRect); - - pixelsMovedPerNode.set(node.node_id, areaOfImpact); - totalAreaOfImpact += areaOfImpact; + static async getTopLayoutShiftElements(trace, context) { + const {impactByNodeId} = await CumulativeLayoutShift.request(trace, context); + + return [...impactByNodeId.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, MAX_LAYOUT_SHIFT_ELEMENTS) + .map(([nodeId, clsContribution]) => { + return { + nodeId: nodeId, + score: clsContribution, + }; }); - - for (const [nodeId, pixelsMoved] of pixelsMovedPerNode.entries()) { - let clsContribution = clsPerNode.get(nodeId) || 0; - clsContribution += (pixelsMoved / totalAreaOfImpact) * event.weightedScore; - clsPerNode.set(nodeId, clsContribution); - } - }); - - const topFive = [...clsPerNode.entries()] - .sort((a, b) => b[1] - a[1]) - .slice(0, 5) - .map(([nodeId, clsContribution]) => { - return { - nodeId: nodeId, - score: clsContribution, - }; - }); - - return topFive; } /** @@ -265,7 +215,7 @@ class TraceElements extends BaseGatherer { const {mainThreadEvents} = processedTrace; const lcpNodeData = await TraceElements.getLcpElement(trace, context); - const clsNodeData = TraceElements.getTopLayoutShiftElements(processedTrace); + const clsNodeData = await TraceElements.getTopLayoutShiftElements(trace, context); const animatedElementData = await this.getAnimatedElements(mainThreadEvents); const responsivenessElementData = await TraceElements.getResponsivenessElement(trace, context); diff --git a/core/lib/rect-helpers.js b/core/lib/rect-helpers.js index ddfcc25a018a..03d370b56d1d 100644 --- a/core/lib/rect-helpers.js +++ b/core/lib/rect-helpers.js @@ -232,6 +232,20 @@ function allRectsContainedWithinEachOther(rectListA, rectListB) { return true; } +/** + * @param {Array} rect + * @return {LH.Artifacts.Rect} + */ +function traceRectToLHRect(rect) { + const rectArgs = { + x: rect[0], + y: rect[1], + width: rect[2], + height: rect[3], + }; + return addRectTopAndBottom(rectArgs); +} + export { rectContainsPoint, rectContains, @@ -248,4 +262,5 @@ export { allRectsContainedWithinEachOther, filterOutRectsContainedByOthers, filterOutTinyRects, + traceRectToLHRect, }; diff --git a/core/test/audits/layout-shift-elements-test.js b/core/test/audits/layout-shift-elements-test.js index a3525cade98f..e5e0314b2c29 100644 --- a/core/test/audits/layout-shift-elements-test.js +++ b/core/test/audits/layout-shift-elements-test.js @@ -15,6 +15,11 @@ describe('Performance: layout-shift-elements audit', () => { had_recent_input: false, is_main_frame: true, weighted_score_delta: 0.3, + impacted_nodes: [{ + node_id: 1, + old_rect: [0, 0, 1, 1], + new_rect: [0, 0, 2, 2], + }], }, frame: 'ROOT_FRAME', }, @@ -26,6 +31,11 @@ describe('Performance: layout-shift-elements audit', () => { had_recent_input: false, is_main_frame: true, weighted_score_delta: 0.1, + impacted_nodes: [{ + node_id: 1, + old_rect: [0, 0, 1, 1], + new_rect: [0, 0, 2, 2], + }], }, frame: 'ROOT_FRAME', }, @@ -66,20 +76,90 @@ describe('Performance: layout-shift-elements audit', () => { nodeLabel: 'My Test Label', snippet: '

', }, - score: 0.1, + score: 0.3, }; + const trace = createTestTrace({}); + for (let i = 1; i <= 4; ++i) { + trace.traceEvents.push({ + args: { + data: { + had_recent_input: false, + is_main_frame: true, + weighted_score_delta: 0.3, + impacted_nodes: [{ + node_id: i, + old_rect: [0, 0, 1, 1], + new_rect: [0, 0, 2, 2], + }], + }, + frame: 'ROOT_FRAME', + }, + name: 'LayoutShift', + cat: 'loading', + }); + } const artifacts = { - traces: {defaultPass: createTestTrace({})}, + traces: {defaultPass: trace}, TraceElements: Array(4).fill(clsElement), }; const auditResult = await LayoutShiftElementsAudit.audit(artifacts, {computedCache: new Map()}); - expect(auditResult.score).toEqual(1); + expect(auditResult.score).toEqual(0); expect(auditResult.notApplicable).toEqual(false); expect(auditResult.displayValue).toBeDisplayString('4 elements found'); expect(auditResult.details.items).toHaveLength(4); }); + it('aggregates elements missing from TraceElements', async () => { + const clsElement = { + traceEventType: 'layout-shift', + node: { + devtoolsNodePath: '1,HTML,3,BODY,5,DIV,0,HEADER', + selector: 'div.l-header > div.chorus-emc__content', + nodeLabel: 'My Test Label', + snippet: '

', + }, + score: 0.1, + }; + const trace = createTestTrace({}); + + // Create 20 shift events on 20 unique elements + for (let i = 1; i <= 20; ++i) { + trace.traceEvents.push({ + args: { + data: { + had_recent_input: false, + is_main_frame: true, + weighted_score_delta: 0.1, + impacted_nodes: [{ + node_id: i, + old_rect: [0, 0, 1, 1], + new_rect: [0, 0, 2, 2], + }], + }, + frame: 'ROOT_FRAME', + }, + name: 'LayoutShift', + cat: 'loading', + }); + } + const artifacts = { + traces: {defaultPass: trace}, + // Only create element data for the first 15 elements + TraceElements: Array(15).fill(clsElement), + }; + + const auditResult = await LayoutShiftElementsAudit.audit(artifacts, {computedCache: new Map()}); + expect(auditResult.score).toEqual(0); + expect(auditResult.notApplicable).toEqual(false); + expect(auditResult.displayValue).toBeDisplayString('20 elements found'); + expect(auditResult.details.items).toHaveLength(16); + + expect(auditResult.details.items[15].node.type).toEqual('code'); + expect(auditResult.details.items[15].node.value).toBeDisplayString('Other'); + expect(auditResult.details.items[15].score).toBeCloseTo(5 * 0.1); + }); + it('correctly handles when there are no CLS elements to show', async () => { const artifacts = { traces: {defaultPass: createTestTrace({})}, diff --git a/core/test/computed/metrics/cumulative-layout-shift-test.js b/core/test/computed/metrics/cumulative-layout-shift-test.js index 0e5c94a583f2..85b1c5d00ff5 100644 --- a/core/test/computed/metrics/cumulative-layout-shift-test.js +++ b/core/test/computed/metrics/cumulative-layout-shift-test.js @@ -26,6 +26,9 @@ describe('Metrics: CLS', () => { expect(result).toEqual({ cumulativeLayoutShift: expect.toBeApproximately(2.268816, 6), cumulativeLayoutShiftMainFrame: expect.toBeApproximately(2.268816, 6), + impactByNodeId: new Map([ + [8, 4.809793674045139], + ]), }); }); @@ -39,6 +42,10 @@ describe('Metrics: CLS', () => { expect(result).toEqual({ cumulativeLayoutShift: 0.026463014612806653, cumulativeLayoutShiftMainFrame: 0.0011656245471340055, + impactByNodeId: new Map([ + [7, 0.026463014612806653], + [8, 0.0011656245471340055], + ]), }); }); @@ -47,6 +54,7 @@ describe('Metrics: CLS', () => { expect(result).toEqual({ cumulativeLayoutShift: 0, cumulativeLayoutShiftMainFrame: 0, + impactByNodeId: new Map(), }); }); }); @@ -120,6 +128,7 @@ describe('Metrics: CLS', () => { expect(result).toEqual({ cumulativeLayoutShift: 4, cumulativeLayoutShiftMainFrame: 4, + impactByNodeId: new Map(), }); }); @@ -136,6 +145,7 @@ describe('Metrics: CLS', () => { expect(result).toEqual({ cumulativeLayoutShift: 3, cumulativeLayoutShiftMainFrame: 3, + impactByNodeId: new Map(), }); }); @@ -153,6 +163,7 @@ describe('Metrics: CLS', () => { expect(result).toEqual({ cumulativeLayoutShift: 0.75, cumulativeLayoutShiftMainFrame: 0.75, + impactByNodeId: new Map(), }); }); @@ -177,6 +188,7 @@ describe('Metrics: CLS', () => { expect(result).toEqual({ cumulativeLayoutShift: 1.0625, cumulativeLayoutShiftMainFrame: 1.0625, + impactByNodeId: new Map(), }); }); @@ -194,6 +206,7 @@ describe('Metrics: CLS', () => { expect(result).toEqual({ cumulativeLayoutShift: 3.75, // 30 * 0.125 cumulativeLayoutShiftMainFrame: 3.75, + impactByNodeId: new Map(), }); }); @@ -217,6 +230,7 @@ describe('Metrics: CLS', () => { expect(result).toEqual({ cumulativeLayoutShift: 3, cumulativeLayoutShiftMainFrame: 3, + impactByNodeId: new Map(), }); }); }); @@ -237,6 +251,7 @@ describe('Metrics: CLS', () => { expect(result).toEqual({ cumulativeLayoutShift: 0.75, // Same value as single-frame uniformly distributed. cumulativeLayoutShiftMainFrame: 0.125, // All 1s gaps, so only one event per cluster. + impactByNodeId: new Map(), }); }); @@ -265,6 +280,7 @@ describe('Metrics: CLS', () => { expect(result).toMatchObject({ cumulativeLayoutShift: 4, cumulativeLayoutShiftMainFrame: 2, + impactByNodeId: new Map(), }); }); @@ -334,6 +350,7 @@ describe('Metrics: CLS', () => { expect(result).toMatchObject({ cumulativeLayoutShift: 4, cumulativeLayoutShiftMainFrame: 2, + impactByNodeId: new Map(), }); }); @@ -362,6 +379,7 @@ describe('Metrics: CLS', () => { expect(result).toMatchObject({ cumulativeLayoutShift: 3, cumulativeLayoutShiftMainFrame: 1, + impactByNodeId: new Map(), }); }); }); @@ -381,6 +399,7 @@ describe('Metrics: CLS', () => { expect(result).toMatchObject({ cumulativeLayoutShift: 6, cumulativeLayoutShiftMainFrame: 6, + impactByNodeId: new Map(), }); }); @@ -399,6 +418,7 @@ describe('Metrics: CLS', () => { expect(result).toMatchObject({ cumulativeLayoutShift: 6, cumulativeLayoutShiftMainFrame: 1, + impactByNodeId: new Map(), }); }); @@ -412,6 +432,7 @@ describe('Metrics: CLS', () => { expect(result).toMatchObject({ cumulativeLayoutShift: 2, cumulativeLayoutShiftMainFrame: 2, + impactByNodeId: new Map(), }); }); @@ -425,8 +446,125 @@ describe('Metrics: CLS', () => { expect(result).toMatchObject({ cumulativeLayoutShift: 2, cumulativeLayoutShiftMainFrame: 1, + impactByNodeId: new Map(), }); }); }); }); + + describe('getImpactByNodeId', () => { + it('combines scores for the same nodeId across multiple shift events', () => { + const layoutShiftEvents = [ + { + ts: 1_000_000, + isMainFrame: true, + weightedScore: 1, + impactedNodes: [ + { + new_rect: [0, 0, 200, 200], + node_id: 60, + old_rect: [0, 0, 200, 100], + }, + { + new_rect: [0, 300, 200, 200], + node_id: 25, + old_rect: [0, 100, 200, 100], + }, + ], + }, + { + ts: 2_000_000, + isMainFrame: true, + weightedScore: 0.3, + impactedNodes: [ + { + new_rect: [0, 100, 200, 200], + node_id: 60, + old_rect: [0, 0, 200, 200], + }, + ], + }, + ]; + + const impactByNodeId = CumulativeLayoutShift.getImpactByNodeId(layoutShiftEvents); + expect(Array.from(impactByNodeId.entries())).toEqual([ + [60, 0.7], + [25, 0.6], + ]); + }); + + it('ignores events with no impacted nodes', () => { + const layoutShiftEvents = [ + { + ts: 1_000_000, + isMainFrame: true, + weightedScore: 1, + impactedNodes: [ + { + new_rect: [0, 0, 200, 200], + node_id: 60, + old_rect: [0, 0, 200, 100], + }, + { + new_rect: [0, 300, 200, 200], + node_id: 25, + old_rect: [0, 100, 200, 100], + }, + ], + }, + { + ts: 2_000_000, + isMainFrame: true, + weightedScore: 0.3, + }, + ]; + + const impactByNodeId = CumulativeLayoutShift.getImpactByNodeId(layoutShiftEvents); + expect(Array.from(impactByNodeId.entries())).toEqual([ + [60, 0.4], + [25, 0.6], + ]); + }); + + it('ignores malformed impacted nodes', () => { + const layoutShiftEvents = [ + { + ts: 1_000_000, + isMainFrame: true, + weightedScore: 1, + impactedNodes: [ + { + // Malformed, no old_rect + // Entire weightedScore is therefore attributed to node_id 25 + new_rect: [0, 0, 200, 200], + node_id: 60, + }, + { + new_rect: [0, 300, 200, 200], + node_id: 25, + old_rect: [0, 100, 200, 100], + }, + ], + }, + { + ts: 2_000_000, + isMainFrame: true, + weightedScore: 0.3, + impactedNodes: [ + { + new_rect: [0, 100, 200, 200], + node_id: 60, + old_rect: [0, 0, 200, 200], + }, + ], + }, + ]; + + const impactByNodeId = CumulativeLayoutShift.getImpactByNodeId(layoutShiftEvents); + expect(Array.from(impactByNodeId.entries())).toEqual([ + [25, 1], + [60, 0.3], + ]); + }); + }); }); diff --git a/core/test/gather/gatherers/trace-elements-test.js b/core/test/gather/gatherers/trace-elements-test.js index 60b24ea6c431..07b4df8f208e 100644 --- a/core/test/gather/gatherers/trace-elements-test.js +++ b/core/test/gather/gatherers/trace-elements-test.js @@ -7,7 +7,6 @@ import TraceElementsGatherer from '../../../gather/gatherers/trace-elements.js'; import {createTestTrace, rootFrame} from '../../create-test-trace.js'; import {flushAllTimersAndMicrotasks, readJson, timers} from '../../test-utils.js'; -import {ProcessedTrace} from '../../../computed/processed-trace.js'; import {createMockDriver} from '../mock-driver.js'; const animationTrace = readJson('../../fixtures/artifacts/animation/trace.json', import.meta); @@ -83,11 +82,6 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { return sum; } - function expectEqualFloat(actual, expected) { - const diff = Math.abs(actual - expected); - expect(diff).toBeLessThanOrEqual(Number.EPSILON); - } - it('returns layout shift data sorted by impact area', async () => { const trace = createTestTrace({}); trace.traceEvents.push( @@ -104,170 +98,18 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { }, ]) ); - const processedTrace = await ProcessedTrace.request(trace, {computedCache: new Map()}); - const result = TraceElementsGatherer.getTopLayoutShiftElements(processedTrace); + const result = + await TraceElementsGatherer.getTopLayoutShiftElements(trace, {computedCache: new Map()}); expect(result).toEqual([ {nodeId: 25, score: 0.6}, {nodeId: 60, score: 0.4}, ]); const total = sumScores(result); - expectEqualFloat(total, 1.0); - }); - - it('does not ignore initial trace events with input', async () => { - const trace = createTestTrace({}); - trace.traceEvents.push( - makeLayoutShiftTraceEvent(1, [ - { - new_rect: [0, 0, 200, 200], - node_id: 1, - old_rect: [0, 0, 200, 100], - }, - ], true), - makeLayoutShiftTraceEvent(1, [ - { - new_rect: [0, 0, 200, 200], - node_id: 2, - old_rect: [0, 0, 200, 100], - }, - ], true) - ); - const processedTrace = await ProcessedTrace.request(trace, {computedCache: new Map()}); - - const result = TraceElementsGatherer.getTopLayoutShiftElements(processedTrace); - expect(result).toEqual([ - {nodeId: 1, score: 1}, - {nodeId: 2, score: 1}, - ]); - }); - - it('does ignore later trace events with input', async () => { - const trace = createTestTrace({}); - trace.traceEvents.push( - makeLayoutShiftTraceEvent(1, [ - { - new_rect: [0, 0, 200, 200], - node_id: 1, - old_rect: [0, 0, 200, 100], - }, - ]), - makeLayoutShiftTraceEvent(1, [ - { - new_rect: [0, 0, 200, 200], - node_id: 2, - old_rect: [0, 0, 200, 100], - }, - ], true) - ); - const processedTrace = await ProcessedTrace.request(trace, {computedCache: new Map()}); - - const result = TraceElementsGatherer.getTopLayoutShiftElements(processedTrace); - expect(result).toEqual([ - {nodeId: 1, score: 1}, - ]); + expect(total).toBeCloseTo(1.0); }); - it('correctly ignores trace events with input (complex)', async () => { - const trace = createTestTrace({}); - trace.traceEvents.push( - makeLayoutShiftTraceEvent(1, [ - { - new_rect: [0, 0, 200, 200], - node_id: 1, - old_rect: [0, 0, 200, 100], - }, - ], true), - makeLayoutShiftTraceEvent(1, [ - { - new_rect: [0, 0, 200, 200], - node_id: 2, - old_rect: [0, 0, 200, 100], - }, - ], true), - makeLayoutShiftTraceEvent(1, [ - { - new_rect: [0, 0, 200, 200], - node_id: 3, - old_rect: [0, 0, 200, 100], - }, - ]), - makeLayoutShiftTraceEvent(1, [ - { - new_rect: [0, 0, 200, 200], - node_id: 4, - old_rect: [0, 0, 200, 100], - }, - ]), - makeLayoutShiftTraceEvent(1, [ - { - new_rect: [0, 0, 200, 200], - node_id: 5, - old_rect: [0, 0, 200, 100], - }, - ], true), - makeLayoutShiftTraceEvent(1, [ - { - new_rect: [0, 0, 200, 200], - node_id: 6, - old_rect: [0, 0, 200, 100], - }, - ], true), - makeLayoutShiftTraceEvent(1, [ - { - new_rect: [0, 0, 200, 200], - node_id: 7, - old_rect: [0, 0, 200, 100], - }, - ]) - ); - const processedTrace = await ProcessedTrace.request(trace, {computedCache: new Map()}); - - const result = TraceElementsGatherer.getTopLayoutShiftElements(processedTrace); - expect(result).toEqual([ - {nodeId: 1, score: 1}, - {nodeId: 2, score: 1}, - {nodeId: 3, score: 1}, - {nodeId: 4, score: 1}, - {nodeId: 7, score: 1}, - ]); - }); - - it('combines scores for the same nodeId accross multiple shift events', async () => { - const trace = createTestTrace({}); - trace.traceEvents.push( - makeLayoutShiftTraceEvent(1, [ - { - new_rect: [0, 0, 200, 200], - node_id: 60, - old_rect: [0, 0, 200, 100], - }, - { - new_rect: [0, 300, 200, 200], - node_id: 25, - old_rect: [0, 100, 200, 100], - }, - ]), - makeLayoutShiftTraceEvent(0.3, [ - { - new_rect: [0, 100, 200, 200], - node_id: 60, - old_rect: [0, 0, 200, 200], - }, - ]) - ); - const processedTrace = await ProcessedTrace.request(trace, {computedCache: new Map()}); - - const result = TraceElementsGatherer.getTopLayoutShiftElements(processedTrace); - expect(result).toEqual([ - {nodeId: 60, score: 0.7}, - {nodeId: 25, score: 0.6}, - ]); - const total = sumScores(result); - expectEqualFloat(total, 1.3); - }); - - it('returns only the top five values', async () => { + it('returns only the top 15 values', async () => { const trace = createTestTrace({}); trace.traceEvents.push( makeLayoutShiftTraceEvent(1, [ @@ -310,20 +152,39 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { node_id: 7, old_rect: [0, 0, 100, 100], }, - ]) + ]), + makeLayoutShiftTraceEvent(1, + Array(10).fill({ + new_rect: [0, 0, 100, 200], + old_rect: [0, 0, 100, 100], + }).map((event, i) => ({ + ...event, + node_id: i + 8, + })) + ) ); - const processedTrace = await ProcessedTrace.request(trace, {computedCache: new Map()}); - const result = TraceElementsGatherer.getTopLayoutShiftElements(processedTrace); + const result = + await TraceElementsGatherer.getTopLayoutShiftElements(trace, {computedCache: new Map()}); expect(result).toEqual([ {nodeId: 3, score: 1.0}, {nodeId: 1, score: 0.5}, {nodeId: 2, score: 0.5}, {nodeId: 6, score: 0.25}, {nodeId: 7, score: 0.25}, + {nodeId: 4, score: 0.125}, + {nodeId: 5, score: 0.125}, + {nodeId: 8, score: 0.1}, + {nodeId: 9, score: 0.1}, + {nodeId: 10, score: 0.1}, + {nodeId: 11, score: 0.1}, + {nodeId: 12, score: 0.1}, + {nodeId: 13, score: 0.1}, + {nodeId: 14, score: 0.1}, + {nodeId: 15, score: 0.1}, ]); const total = sumScores(result); - expectEqualFloat(total, 2.5); + expect(total).toBeCloseTo(3.55); }); }); diff --git a/core/test/lib/rect-helpers-test.js b/core/test/lib/rect-helpers-test.js index 9b794538502b..d57f89b1e3ee 100644 --- a/core/test/lib/rect-helpers-test.js +++ b/core/test/lib/rect-helpers-test.js @@ -11,6 +11,7 @@ import { getRectAtCenter, allRectsContainedWithinEachOther, getBoundingRectWithPadding, + traceRectToLHRect, } from '../../lib/rect-helpers.js'; describe('Rect Helpers', () => { @@ -67,6 +68,18 @@ describe('Rect Helpers', () => { ); }); + it('traceRectToLHRect', () => { + const rect = traceRectToLHRect([50, 25, 100, 200]); + expect(rect).toEqual( + addRectTopAndBottom({ + x: 50, + y: 25, + width: 100, + height: 200, + }) + ); + }); + describe('allRectsContainedWithinEachOther', () => { it('Returns true if the rect lists are both empty', () => { expect(allRectsContainedWithinEachOther([], [])).toBe(true);