Skip to content

Commit deabb5e

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
RPP: fix CLS Culprits Insight not rendering
This CL fixes a bug in the CLS Culprits insight where it tried to read `this.bounds` which was undefined. I think TS expects the `get` to exist when the `set` does (we hit this a lot in our components). To fix this I added `get bounds`. I then added a unit test to verify the fix and to ensure it renders. I then went down a bit of a rabbit-hole of styling; the <li> elements for the culprits were not wrapped in a <ul> so I added that and tidied up some of the margins. Bug: none Change-Id: I38dad120760f2f8bc13c63c31f07dcafe11703eb Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6074609 Commit-Queue: Connor Clark <[email protected]> Reviewed-by: Connor Clark <[email protected]> Auto-Submit: Jack Franklin <[email protected]>
1 parent 39e2a3b commit deabb5e

File tree

6 files changed

+82
-7
lines changed

6 files changed

+82
-7
lines changed

front_end/panels/timeline/components/insights/BUILD.gn

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ devtools_entrypoint("bundle") {
6969
ts_library("unittests") {
7070
testonly = true
7171

72-
sources = [ "BaseInsightComponent.test.ts" ]
72+
sources = [
73+
"BaseInsightComponent.test.ts",
74+
"CLSCulprits.test.ts",
75+
]
7376

7477
deps = [
7578
":bundle",

front_end/panels/timeline/components/insights/BaseInsightComponent.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ export abstract class BaseInsightComponent<T extends InsightModel<{}>> extends H
112112
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
113113
}
114114

115+
get bounds(): Trace.Types.Timing.TraceWindowMicroSeconds|null {
116+
return this.data.bounds;
117+
}
118+
115119
set bounds(bounds: Trace.Types.Timing.TraceWindowMicroSeconds|null) {
116120
this.data.bounds = bounds;
117121
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2024 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import {
6+
getCleanTextContentFromElements,
7+
getCleanTextContentFromSingleElement,
8+
renderElementIntoDOM,
9+
} from '../../../../testing/DOMHelpers.js';
10+
import {describeWithEnvironment} from '../../../../testing/EnvironmentHelpers.js';
11+
import {TraceLoader} from '../../../../testing/TraceLoader.js';
12+
import * as Coordinator from '../../../../ui/components/render_coordinator/render_coordinator.js';
13+
14+
import * as Insights from './insights.js';
15+
16+
const coordinator = Coordinator.RenderCoordinator.RenderCoordinator.instance();
17+
describeWithEnvironment('CLSCulprits component', () => {
18+
it('renders unsized image culprits', async function() {
19+
const traceData = await TraceLoader.traceEngine(this, 'unsized-images.json.gz');
20+
const firstNavInsights = traceData.insights?.values().next()?.value;
21+
assert.isOk(firstNavInsights);
22+
const clsModel = firstNavInsights.model.CLSCulprits;
23+
const component = new Insights.CLSCulprits.CLSCulprits();
24+
component.model = clsModel;
25+
component.insightSetKey = firstNavInsights.id;
26+
component.bounds = traceData.parsedTrace.Meta.traceBounds;
27+
component.selected = true;
28+
29+
renderElementIntoDOM(component);
30+
await coordinator.done();
31+
assert.isOk(component.shadowRoot);
32+
33+
const titleText = getCleanTextContentFromSingleElement(component.shadowRoot, '.insight-title');
34+
assert.strictEqual(titleText, 'Layout shift culprits');
35+
36+
const worstClusterText = getCleanTextContentFromSingleElement(component.shadowRoot, '.worst-cluster');
37+
assert.strictEqual(worstClusterText, 'Worst cluster: Layout shift cluster @ 1.37 s');
38+
39+
const culpritsList = component.shadowRoot.querySelector<HTMLUListElement>('.worst-culprits');
40+
assert.isOk(culpritsList);
41+
const culpritsText = getCleanTextContentFromElements(culpritsList, 'li');
42+
// There are two shifts hence the two culprits.
43+
assert.deepEqual(culpritsText, ['Unsized Images', 'Unsized Images']);
44+
});
45+
});

front_end/panels/timeline/components/insights/CLSCulprits.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,14 @@ export class CLSCulprits extends BaseInsightComponent<CLSCulpritsInsightModel> {
150150
return html`
151151
<div class="insight-section">
152152
<span class="worst-cluster">${i18nString(UIStrings.worstCluster)}: <button type="button" class="timeline-link" @click=${() => this.#clickEvent(worstCluster)}>${i18nString(UIStrings.layoutShiftCluster, {PH1: clusterTs})}</button></span>
153-
<p>${i18nString(UIStrings.topCulprits)}:</p>
154-
${culprits.map(culprit => {
155-
return html `
156-
<li>${culprit}</li>
157-
`;
158-
})}
153+
<p class="list-title">${i18nString(UIStrings.topCulprits)}:</p>
154+
<ul class="worst-culprits">
155+
${culprits.map(culprit => {
156+
return html `
157+
<li>${culprit}</li>
158+
`;
159+
})}
160+
</ul>
159161
</div>`;
160162
// clang-format on
161163
}

front_end/panels/timeline/components/insights/baseInsightComponent.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@
8181

8282
.insight-body {
8383
padding: 0 var(--sys-size-6) var(--sys-size-5) var(--sys-size-6);
84+
85+
.list-title {
86+
margin-top: var(--sys-size-4);
87+
margin-bottom: var(--sys-size-3);
88+
}
89+
90+
ul {
91+
/* left padding to bring the list bullets to the right place */
92+
padding: 0 0 0 var(--sys-size-9);
93+
margin: 0;
94+
}
8495
}
8596

8697
.insight-section {

front_end/testing/DOMHelpers.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,16 @@ export function getCleanTextContentFromElements(el: ShadowRoot|HTMLElement, sele
267267
});
268268
}
269269

270+
/**
271+
* Returns the text content for the first element matching the given `selector` within the provided `el`.
272+
* Will error if no element is found matching the selector.
273+
*/
274+
export function getCleanTextContentFromSingleElement(el: ShadowRoot|HTMLElement, selector: string): string {
275+
const element = el.querySelector(selector);
276+
assert.isOk(element, `Could not find element with selector ${selector}`);
277+
return element.textContent ? element.textContent.trim().replace(/[ \n]{2,}/g, ' ') : '';
278+
}
279+
270280
export function assertNodeTextContent(component: NodeText.NodeText.NodeText, expectedContent: string) {
271281
assert.isNotNull(component.shadowRoot);
272282
const content = Array.from(component.shadowRoot.querySelectorAll('span')).map(span => span.textContent).join('');

0 commit comments

Comments
 (0)