Skip to content

Commit e274f6c

Browse files
yanlingwang23Devtools-frontend LUCI CQ
authored and
Devtools-frontend LUCI CQ
committed
Dom Adorner
This CL adds support to show multiple issues through squiggly lines in the DOM tree of Elements Panel. Screenshot:https://imgur.com/a/hKpaXd3 Explainer: https://docs.google.com/document/d/1NIoIQdiSN72HVb4TosfE7tMHNTjEEjsCye_ymqoOXHw/edit?tab=t.0#heading=h.mymnss778pd7 Bug: 378738916 Change-Id: I0b37553c056b429cbac445bdbc35779af81ffdc9 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6249604 Reviewed-by: Philip Pfaffe <[email protected]> Commit-Queue: Changhao Han <[email protected]> Reviewed-by: Changhao Han <[email protected]>
1 parent 8628dfc commit e274f6c

File tree

10 files changed

+187
-37
lines changed

10 files changed

+187
-37
lines changed

front_end/models/issues_manager/SelectElementAccessibilityIssue.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ export class SelectElementAccessibilityIssue extends Issue {
3131
}
3232

3333
getDescription(): MarkdownIssueDescription|null {
34-
if (this.issueDetails.hasDisallowedAttributes &&
35-
(this.issueDetails.selectElementAccessibilityIssueReason !==
36-
Protocol.Audits.SelectElementAccessibilityIssueReason.InteractiveContentOptionChild)) {
34+
if (this.isInteractiveContentAttributesSelectDescendantIssue()) {
3735
return {
3836
file: 'selectElementAccessibilityInteractiveContentAttributesSelectDescendant.md',
3937
links: [],
@@ -58,6 +56,12 @@ export class SelectElementAccessibilityIssue extends Issue {
5856
return this.issueDetails;
5957
}
6058

59+
isInteractiveContentAttributesSelectDescendantIssue(): boolean {
60+
return this.issueDetails.hasDisallowedAttributes &&
61+
(this.issueDetails.selectElementAccessibilityIssueReason !==
62+
Protocol.Audits.SelectElementAccessibilityIssueReason.InteractiveContentOptionChild);
63+
}
64+
6165
static fromInspectorIssue(issuesModel: SDK.IssuesModel.IssuesModel, inspectorIssue: Protocol.Audits.InspectorIssue):
6266
SelectElementAccessibilityIssue[] {
6367
const selectElementAccessibilityIssueDetails = inspectorIssue.details.selectElementAccessibilityIssueDetails;

front_end/panels/elements/ElementIssueUtils.ts

+10
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ const UIStrings = {
6767
* @description Tooltip text shown in the Elements panel when an element has an error.
6868
*/
6969
interactiveContentLegendChild: 'Interactive element inside of a <legend> element',
70+
/**
71+
* @description Tooltip text shown in the Elements panel when an element has an error.
72+
*/
73+
interactiveContentAttributesSelectDescendant: 'Element with invalid attributes within a <select> element',
7074
} as const;
7175

7276
const str_ = i18n.i18n.registerUIStrings('panels/elements/ElementIssueUtils.ts', UIStrings);
@@ -89,6 +93,12 @@ export function getElementIssueDetails(issue: IssuesManager.Issue.Issue): Elemen
8993
}
9094
if (issue instanceof IssuesManager.SelectElementAccessibilityIssue.SelectElementAccessibilityIssue) {
9195
const issueDetails = issue.details();
96+
if (issue.isInteractiveContentAttributesSelectDescendantIssue()) {
97+
return {
98+
tooltip: i18nString(UIStrings.interactiveContentAttributesSelectDescendant),
99+
nodeId: issueDetails.nodeId,
100+
};
101+
}
92102
return {
93103
tooltip: getTooltipFromSelectElementAccessibilityIssue(issueDetails.selectElementAccessibilityIssueReason),
94104
nodeId: issueDetails.nodeId,

front_end/panels/elements/ElementsTreeElement.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
264264
private aiButtonContainer?: HTMLElement;
265265
private contentElement: HTMLElement;
266266
#elementIssues = new Map<string, IssuesManager.Issue.Issue>();
267-
#nodeElementToIssue = new Map<Element, IssuesManager.Issue.Issue>();
267+
#nodeElementToIssue = new Map<Element, IssuesManager.Issue.Issue[]>();
268268
#highlights: Range[] = [];
269269

270270
readonly tagTypeContext: TagTypeContext;
@@ -457,7 +457,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
457457
}
458458
}
459459

460-
get issuesByNodeElement(): Map<Element, IssuesManager.Issue.Issue> {
460+
get issuesByNodeElement(): Map<Element, IssuesManager.Issue.Issue[]> {
461461
return this.#nodeElementToIssue;
462462
}
463463

@@ -468,15 +468,23 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
468468
if (attribute.getElementsByClassName('webkit-html-attribute-name')[0].textContent === name) {
469469
const attributeElement = attribute.getElementsByClassName('webkit-html-attribute-name')[0];
470470
attributeElement.classList.add('violating-element');
471-
this.#nodeElementToIssue.set(attributeElement, issue);
471+
this.#updateNodeElementToIssue(attributeElement, issue);
472472
}
473473
}
474474
}
475475

476476
#highlightTagAsViolating(issue: IssuesManager.Issue.Issue): void {
477477
const tagElement = this.listItemElement.getElementsByClassName('webkit-html-tag-name')[0];
478478
tagElement.classList.add('violating-element');
479-
this.#nodeElementToIssue.set(tagElement, issue);
479+
this.#updateNodeElementToIssue(tagElement, issue);
480+
}
481+
482+
#updateNodeElementToIssue(nodeElement: Element, issue: IssuesManager.Issue.Issue): void {
483+
if (!this.#nodeElementToIssue.has(nodeElement)) {
484+
this.#nodeElementToIssue.set(nodeElement, [issue]);
485+
return;
486+
}
487+
this.#nodeElementToIssue.get(nodeElement)?.push(issue);
480488
}
481489

482490
expandedChildrenLimit(): number {

front_end/panels/elements/ElementsTreeOutline.test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,30 @@ describeWithMockConnection('ElementsTreeOutline', () => {
173173
tagElement.classList.remove('violating-element');
174174
}
175175

176+
// Test that multiple issues being added to the tree element.
177+
{
178+
const inspectorIssue = {
179+
code: Protocol.Audits.InspectorIssueCode.GenericIssue,
180+
details: {
181+
genericIssueDetails: {
182+
errorType: Protocol.Audits.GenericIssueErrorType.FormEmptyIdAndNameAttributesForInputError,
183+
frameId: 'main' as Protocol.Page.FrameId,
184+
violatingNodeId: 2 as Protocol.DOM.BackendNodeId,
185+
},
186+
},
187+
};
188+
const issue = IssuesManager.GenericIssue.GenericIssue.fromInspectorIssue(mockModel, inspectorIssue)[0];
189+
issuesManager.dispatchEventToListeners(
190+
IssuesManager.IssuesManager.Events.ISSUE_ADDED, {issuesModel: mockModel, issue});
191+
await deferredDOMNodeStub();
192+
const tagElement = treeElement.listItemElement.getElementsByClassName('webkit-html-tag-name')[0];
193+
assert.isTrue(tagElement.classList.contains('violating-element'));
194+
const issues = treeElement.issuesByNodeElement.get(tagElement);
195+
assert.strictEqual(issues?.length, 3);
196+
// Reset tag to prepare for subsequent tests.
197+
tagElement.classList.remove('violating-element');
198+
}
199+
176200
// Test that non-supported issue won't be added to the tree element.
177201
{
178202
const inspectorIssue = {

front_end/panels/elements/ElementsTreeOutline.ts

+28-29
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import * as CodeHighlighter from '../../ui/components/code_highlighter/code_high
4242
import * as IconButton from '../../ui/components/icon_button/icon_button.js';
4343
import * as IssueCounter from '../../ui/components/issue_counter/issue_counter.js';
4444
import * as UI from '../../ui/legacy/legacy.js';
45+
import {html, nothing, render} from '../../ui/lit/lit.js';
4546
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
4647

4748
import * as ElementsComponents from './components/components.js';
@@ -71,6 +72,10 @@ const UIStrings = {
7172
*@description Link text content in Elements Tree Outline of the Elements panel
7273
*/
7374
reveal: 'reveal',
75+
/**
76+
* @description Text for popover that directs to Issues panel
77+
*/
78+
viewIssue: 'View Issue:',
7479
} as const;
7580
const str_ = i18n.i18n.registerUIStrings('panels/elements/ElementsTreeOutline.ts', UIStrings);
7681
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
@@ -106,7 +111,7 @@ export class ElementsTreeOutline extends
106111
#topLayerContainerByParent = new Map<UI.TreeOutline.TreeElement, TopLayerContainer>();
107112
#issuesManager?: IssuesManager.IssuesManager.IssuesManager;
108113
#popupHelper?: UI.PopoverHelper.PopoverHelper;
109-
#nodeElementToIssue = new Map<Element, IssuesManager.Issue.Issue>();
114+
#nodeElementToIssues = new Map<Element, IssuesManager.Issue.Issue[]>();
110115

111116
constructor(omitRootDOMNode?: boolean, selectEnabled?: boolean, hideGutter?: boolean) {
112117
super();
@@ -194,40 +199,34 @@ export class ElementsTreeOutline extends
194199
return null;
195200
}
196201

197-
const issue = this.#nodeElementToIssue.get(hoveredNode);
198-
if (!issue) {
199-
return null;
200-
}
201-
const elementIssueDetails = getElementIssueDetails(issue);
202-
if (!elementIssueDetails) {
202+
const issues = this.#nodeElementToIssues.get(hoveredNode);
203+
if (!issues) {
203204
return null;
204205
}
205-
const issueKindIcon = new IconButton.Icon.Icon();
206-
issueKindIcon.data = IssueCounter.IssueCounter.getIssueKindIconData(issue.getKind());
207-
issueKindIcon.style.cursor = 'pointer';
208-
const viewIssueElement = document.createElement('a');
209-
viewIssueElement.href = '#';
210-
viewIssueElement.textContent = 'View issue:';
211-
212-
const issueTitle = document.createElement('span');
213-
issueTitle.textContent = elementIssueDetails.tooltip;
214-
215-
const element = document.createElement('div');
216-
element.appendChild(issueKindIcon);
217-
element.appendChild(viewIssueElement);
218-
element.appendChild(issueTitle);
219-
element.style.display = 'flex';
220-
element.style.alignItems = 'center';
221-
element.style.gap = '5px';
222206

223207
return {
224208
box: hoveredNode.boxInWindow(),
225209
show: async (popover: UI.GlassPane.GlassPane) => {
226210
popover.setIgnoreLeftMargin(true);
227-
const openIssueEvent = (): Promise<void> => Common.Revealer.reveal(issue);
228-
viewIssueElement.addEventListener('click', () => openIssueEvent());
229-
issueKindIcon.addEventListener('click', () => openIssueEvent());
230-
popover.contentElement.appendChild(element);
211+
// clang-format off
212+
render(html`
213+
<div class="squiggles-content">
214+
${issues.map(issue => {
215+
const elementIssueDetails = getElementIssueDetails(issue);
216+
if (!elementIssueDetails) {
217+
// This shouldn't happen, but add this if check to pass ts check.
218+
return nothing;
219+
}
220+
const issueKindIconData = IssueCounter.IssueCounter.getIssueKindIconData(issue.getKind());
221+
const openIssueEvent = (): Promise<void> => Common.Revealer.reveal(issue);
222+
return html`
223+
<div class="squiggles-content-item">
224+
<devtools-icon .data=${issueKindIconData} @click=${openIssueEvent}></devtools-icon>
225+
<x-link class="link" @click=${openIssueEvent}>${i18nString(UIStrings.viewIssue)}</x-link>
226+
<span>${elementIssueDetails.tooltip}</span>
227+
</div>`;})}
228+
</div>`, popover.contentElement);
229+
// clang-format on
231230
return true;
232231
},
233232
};
@@ -276,7 +275,7 @@ export class ElementsTreeOutline extends
276275
const treeElementNodeElementsToIssue = treeElement.issuesByNodeElement;
277276
// This element could be the treeElement tags name or an attribute.
278277
for (const [element, issue] of treeElementNodeElementsToIssue) {
279-
this.#nodeElementToIssue.set(element, issue);
278+
this.#nodeElementToIssues.set(element, issue);
280279
}
281280
}
282281
}

front_end/ui/legacy/popover.css

+16
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,19 @@
2222
.widget.has-padding {
2323
padding: 6px;
2424
}
25+
26+
.squiggles-content {
27+
display: flex;
28+
flex-direction: column;
29+
gap: 5px;
30+
}
31+
32+
.squiggles-content-item {
33+
display: flex;
34+
align-items: center;
35+
gap: 5px;
36+
37+
& devtools-icon {
38+
cursor: pointer;
39+
}
40+
}

front_end/ui/visual_logging/KnownContextValues.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1292,6 +1292,7 @@ export const knownContextValues = new Set([
12921292
'elements.hide-element',
12931293
'elements.image-preview',
12941294
'elements.invalid-property-decl-popover',
1295+
'elements.issue',
12951296
'elements.jump-to-style',
12961297
'elements.layout',
12971298
'elements.length-popover',

test/e2e/elements/violating-element-and-attributes_test.ts

+62-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import {assert} from 'chai';
66

7-
import {click, enableExperiment, waitFor, waitForMany} from '../../shared/helper.js';
7+
import {click, enableExperiment, goToResource, waitFor, waitForMany} from '../../shared/helper.js';
88
import {expandSelectedNodeRecursively, goToResourceAndWaitForStyleSection} from '../helpers/elements-helpers.js';
99

1010
/**
@@ -53,3 +53,64 @@ describe.skip('[crbug.com/1399414]: Element has violating properties', function(
5353
assert.strictEqual(issueTitle, 'A form field element should have an id or name attribute');
5454
});
5555
});
56+
57+
describe('The elements panel', function() {
58+
beforeEach(async function() {
59+
await enableExperiment('highlight-errors-elements-panel');
60+
await goToResource('elements/select-element-with-issues.html');
61+
await expandSelectedNodeRecursively();
62+
});
63+
64+
it('displays squiggly lines on elements with issue', async () => {
65+
const elements = await waitForMany('.violating-element', 2);
66+
const violatingElement = await elements[0].evaluate(node => node.textContent);
67+
assert.strictEqual(violatingElement, 'button');
68+
});
69+
70+
it('displays multiple issues when hoverring over squiggly line', async () => {
71+
const elements = await waitForMany('.violating-element', 2);
72+
const violatingElement = elements[1];
73+
await violatingElement.hover();
74+
const popupParent = await waitFor('div.vbox.flex-auto.no-pointer-events');
75+
const popupText = await popupParent.evaluate(async (node: Element) => {
76+
if (!node.shadowRoot) {
77+
throw new Error('Node shadow root not found.');
78+
}
79+
const popup = node.shadowRoot.querySelector('div.widget.has-padding');
80+
if (!popup) {
81+
throw new Error('Popup not found.');
82+
}
83+
const spans = popup.querySelectorAll('span');
84+
return Array.from(spans).map(span => span.textContent);
85+
});
86+
assert.strictEqual(popupText[0], 'Element with invalid attributes within a <select> element');
87+
assert.strictEqual(popupText[1], 'No label associated with a form field');
88+
});
89+
90+
it('navigates to issues panel when hovering over problematic element', async () => {
91+
const elements = await waitForMany('.violating-element', 2);
92+
const violatingElement = elements[0];
93+
await violatingElement.hover();
94+
const popupParent = await waitFor('div.vbox.flex-auto.no-pointer-events');
95+
const popupText = await popupParent.evaluate(async (node: Element) => {
96+
if (!node.shadowRoot) {
97+
throw new Error('Node shadow root not found.');
98+
}
99+
const popup = node.shadowRoot.querySelector('div.widget.has-padding');
100+
if (!popup) {
101+
throw new Error('Popup not found.');
102+
}
103+
const span = popup.querySelector('span');
104+
if (!span) {
105+
throw new Error('Span not found.');
106+
}
107+
return span.textContent;
108+
});
109+
110+
assert.strictEqual(popupText, 'Interactive element inside of a <legend> element');
111+
await click('div.widget.has-padding x-link');
112+
const highlitedIssue = await waitFor('.issue .header .title');
113+
const issueTitle = await highlitedIssue.evaluate(async (node: Element) => node.textContent);
114+
assert.strictEqual(issueTitle, 'Interactive element inside of a <legend> element');
115+
});
116+
});

test/e2e/resources/elements/BUILD.gn

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ copy_to_gen("elements") {
5454
"page-error.html",
5555
"quirks-mode-iframes.html",
5656
"quirks-mode.html",
57+
"select-element-with-issues.html",
5758
"selection-after-delete.html",
5859
"shadow-dom-modify-chardata.html",
5960
"shadow-roots.html",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<style>
5+
select, ::picker(select) {
6+
appearance: base-select;
7+
}
8+
</style>
9+
</head>
10+
<body>
11+
<select id="interactive-content-legend-child">
12+
<optgroup>
13+
<legend>
14+
<button></button>
15+
</legend>
16+
</optgroup>
17+
</select>
18+
<select id="disallowed-select-child">
19+
<optgroup>
20+
<legend>
21+
<label tabindex="1"></label>
22+
</legend>
23+
</optgroup>
24+
</select>
25+
</body>
26+
</html>

0 commit comments

Comments
 (0)