Skip to content

Commit 4a3066a

Browse files
Ana Sollano KimDevtools-frontend LUCI CQ
authored andcommitted
[Select Element Accessibility Issue] Add issues & tests, part 2
This CL adds `SelectElementAccessibilityIssue` and its descriptions to DevTools. User metrics, unit tests, and E2E tests are added to verify behavior. Doc: https://docs.google.com/document/d/1XZGVjR8uBkgNt6Uhg7E09Veo0fZUgh6eHJQPfLEYe_k/edit?usp=sharing Blink change: https://chromium-review.googlesource.com/c/chromium/src/+/6079873 Bug: 354032791, 347890366 Change-Id: I1d29540bc503c678a100b607565e7e65b64a6463 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6099709 Commit-Queue: Ana Sollano Kim <[email protected]> Reviewed-by: Changhao Han <[email protected]> Reviewed-by: Alex Rudenko <[email protected]>
1 parent aee920d commit 4a3066a

27 files changed

+602
-1
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,12 @@ grd_files_release_sources = [
507507
"front_end/models/issues_manager/descriptions/placeholderDescriptionForInvisibleIssues.md",
508508
"front_end/models/issues_manager/descriptions/propertyRuleInvalidNameIssue.md",
509509
"front_end/models/issues_manager/descriptions/propertyRuleIssue.md",
510+
"front_end/models/issues_manager/descriptions/selectElementAccessibilityDisallowedOptGroupChild.md",
511+
"front_end/models/issues_manager/descriptions/selectElementAccessibilityDisallowedSelectChild.md",
512+
"front_end/models/issues_manager/descriptions/selectElementAccessibilityInteractiveContentAttributesSelectDescendant.md",
513+
"front_end/models/issues_manager/descriptions/selectElementAccessibilityInteractiveContentLegendChild.md",
514+
"front_end/models/issues_manager/descriptions/selectElementAccessibilityInteractiveContentOptionChild.md",
515+
"front_end/models/issues_manager/descriptions/selectElementAccessibilityNonPhrasingContentOptionChild.md",
510516
"front_end/models/issues_manager/descriptions/sharedArrayBuffer.md",
511517
"front_end/models/issues_manager/descriptions/sharedDictionaryUseErrorCrossOriginNoCorsRequest.md",
512518
"front_end/models/issues_manager/descriptions/sharedDictionaryUseErrorDictionaryLoadFailure.md",
@@ -1014,6 +1020,7 @@ grd_files_debug_sources = [
10141020
"front_end/models/issues_manager/PropertyRuleIssue.js",
10151021
"front_end/models/issues_manager/QuirksModeIssue.js",
10161022
"front_end/models/issues_manager/RelatedIssue.js",
1023+
"front_end/models/issues_manager/SelectElementAccessibilityIssue.js",
10171024
"front_end/models/issues_manager/SharedArrayBufferIssue.js",
10181025
"front_end/models/issues_manager/SharedDictionaryIssue.js",
10191026
"front_end/models/issues_manager/SourceFrameIssuesManager.js",
@@ -1448,6 +1455,7 @@ grd_files_debug_sources = [
14481455
"front_end/panels/explain/components/consoleInsightSourcesList.css.js",
14491456
"front_end/panels/issues/AffectedBlockedByResponseView.js",
14501457
"front_end/panels/issues/AffectedCookiesView.js",
1458+
"front_end/panels/issues/AffectedDescendantsWithinSelectElementView.js",
14511459
"front_end/panels/issues/AffectedDirectivesView.js",
14521460
"front_end/panels/issues/AffectedDocumentsInQuirksModeView.js",
14531461
"front_end/panels/issues/AffectedElementsView.js",

front_end/core/host/UserMetrics.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1112,8 +1112,13 @@ export enum IssueCreated {
11121112
'CookieIssue::WarnThirdPartyPhaseout::SetCookie' = 83,
11131113
'CookieIssue::ExcludeThirdPartyPhaseout::ReadCookie' = 84,
11141114
'CookieIssue::ExcludeThirdPartyPhaseout::SetCookie' = 85,
1115+
'SelectElementAccessibilityIssue::DisallowedSelectChild' = 86,
1116+
'SelectElementAccessibilityIssue::DisallowedOptGroupChild' = 87,
1117+
'SelectElementAccessibilityIssue::NonPhrasingContentOptionChild' = 88,
1118+
'SelectElementAccessibilityIssue::InteractiveContentOptionChild' = 89,
1119+
'SelectElementAccessibilityIssue::InteractiveContentLegendChild' = 90,
11151120
/* eslint-enable @typescript-eslint/naming-convention */
1116-
MAX_VALUE = 86,
1121+
MAX_VALUE = 91,
11171122
}
11181123

11191124
export const enum DeveloperResourceLoaded {

front_end/models/issues_manager/BUILD.gn

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ devtools_module("issues_manager") {
3535
"PropertyRuleIssue.ts",
3636
"QuirksModeIssue.ts",
3737
"RelatedIssue.ts",
38+
"SelectElementAccessibilityIssue.ts",
3839
"SharedArrayBufferIssue.ts",
3940
"SharedDictionaryIssue.ts",
4041
"SourceFrameIssuesManager.ts",
@@ -180,6 +181,12 @@ devtools_issue_description_files = [
180181
"propertyRuleIssue.md",
181182
"propertyRuleInvalidNameIssue.md",
182183
"SameSiteWarnStrictLaxDowngradeStrict.md",
184+
"selectElementAccessibilityDisallowedOptGroupChild.md",
185+
"selectElementAccessibilityDisallowedSelectChild.md",
186+
"selectElementAccessibilityInteractiveContentAttributesSelectDescendant.md",
187+
"selectElementAccessibilityInteractiveContentLegendChild.md",
188+
"selectElementAccessibilityInteractiveContentOptionChild.md",
189+
"selectElementAccessibilityNonPhrasingContentOptionChild.md",
183190
"sharedArrayBuffer.md",
184191
"stylesheetLateImport.md",
185192
"stylesheetRequestFailed.md",
@@ -258,6 +265,7 @@ ts_library("unittests") {
258265
"MarkdownIssueDescription.test.ts",
259266
"PropertyRuleIssue.test.ts",
260267
"RelatedIssue.test.ts",
268+
"SelectElementAccessibilityIssue.test.ts",
261269
"SharedDictionaryIssue.test.ts",
262270
"StylesheetLoadingIssue.test.ts",
263271
]

front_end/models/issues_manager/IssuesManager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {LowTextContrastIssue} from './LowTextContrastIssue.js';
2424
import {MixedContentIssue} from './MixedContentIssue.js';
2525
import {PropertyRuleIssue} from './PropertyRuleIssue.js';
2626
import {QuirksModeIssue} from './QuirksModeIssue.js';
27+
import {SelectElementAccessibilityIssue} from './SelectElementAccessibilityIssue.js';
2728
import {SharedArrayBufferIssue} from './SharedArrayBufferIssue.js';
2829
import {SharedDictionaryIssue} from './SharedDictionaryIssue.js';
2930
import {SourceFrameIssuesManager} from './SourceFrameIssuesManager.js';
@@ -123,6 +124,10 @@ const issueCodeHandlers = new Map<
123124
Protocol.Audits.InspectorIssueCode.CookieDeprecationMetadataIssue,
124125
CookieDeprecationMetadataIssue.fromInspectorIssue,
125126
],
127+
[
128+
Protocol.Audits.InspectorIssueCode.SelectElementAccessibilityIssue,
129+
SelectElementAccessibilityIssue.fromInspectorIssue,
130+
],
126131
]);
127132

128133
/**
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright 2025 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 type * as SDK from '../../core/sdk/sdk.js';
6+
import * as Protocol from '../../generated/protocol.js';
7+
import {describeWithLocale} from '../../testing/EnvironmentHelpers.js';
8+
import {MockIssuesModel} from '../../testing/MockIssuesModel.js';
9+
import * as IssuesManager from '../issues_manager/issues_manager.js';
10+
11+
describeWithLocale('SelectElementAccessibilityIssue', () => {
12+
const mockModel = new MockIssuesModel([]) as unknown as SDK.IssuesModel.IssuesModel;
13+
14+
function createProtocolIssueWithoutDetails(): Protocol.Audits.InspectorIssue {
15+
return {
16+
code: Protocol.Audits.InspectorIssueCode.SelectElementAccessibilityIssue,
17+
details: {},
18+
};
19+
}
20+
21+
function createProtocolIssueWithDetails(
22+
selectElementAccessibilityIssueDetails: Protocol.Audits.SelectElementAccessibilityIssueDetails):
23+
Protocol.Audits.InspectorIssue {
24+
return {
25+
code: Protocol.Audits.InspectorIssueCode.SelectElementAccessibilityIssue,
26+
details: {selectElementAccessibilityIssueDetails},
27+
};
28+
}
29+
30+
it('can be created for various reasons', () => {
31+
const reasons = [
32+
Protocol.Audits.SelectElementAccessibilityIssueReason.DisallowedSelectChild,
33+
Protocol.Audits.SelectElementAccessibilityIssueReason.DisallowedOptGroupChild,
34+
Protocol.Audits.SelectElementAccessibilityIssueReason.NonPhrasingContentOptionChild,
35+
Protocol.Audits.SelectElementAccessibilityIssueReason.InteractiveContentOptionChild,
36+
Protocol.Audits.SelectElementAccessibilityIssueReason.InteractiveContentLegendChild,
37+
];
38+
for (const reason of reasons) {
39+
const issueDetails = {
40+
nodeId: 1 as Protocol.DOM.BackendNodeId,
41+
selectElementAccessibilityIssueReason: reason,
42+
hasDisallowedAttributes: false,
43+
};
44+
const issue = createProtocolIssueWithDetails(issueDetails);
45+
const selectIssues =
46+
IssuesManager.SelectElementAccessibilityIssue.SelectElementAccessibilityIssue.fromInspectorIssue(
47+
mockModel, issue);
48+
assert.lengthOf(selectIssues, 1);
49+
const selectIssue = selectIssues[0];
50+
51+
assert.strictEqual(selectIssue.getCategory(), IssuesManager.Issue.IssueCategory.OTHER);
52+
assert.deepEqual(selectIssue.details(), issueDetails);
53+
assert.strictEqual(selectIssue.getKind(), IssuesManager.Issue.IssueKind.PAGE_ERROR);
54+
assert.isNotNull(selectIssue.getDescription());
55+
}
56+
});
57+
58+
it('adds a disallowed select child issue without details', () => {
59+
const inspectorIssueWithoutGenericDetails = createProtocolIssueWithoutDetails();
60+
const selectIssues =
61+
IssuesManager.SelectElementAccessibilityIssue.SelectElementAccessibilityIssue.fromInspectorIssue(
62+
mockModel, inspectorIssueWithoutGenericDetails);
63+
64+
assert.isEmpty(selectIssues);
65+
});
66+
67+
it('adds an interactive content attributes select child issue with valid details', () => {
68+
const issueDetails = {
69+
nodeId: 1 as Protocol.DOM.BackendNodeId,
70+
selectElementAccessibilityIssueReason:
71+
Protocol.Audits.SelectElementAccessibilityIssueReason.InteractiveContentOptionChild,
72+
hasDisallowedAttributes: true,
73+
};
74+
const issue = createProtocolIssueWithDetails(issueDetails);
75+
const selectIssues =
76+
IssuesManager.SelectElementAccessibilityIssue.SelectElementAccessibilityIssue.fromInspectorIssue(
77+
mockModel, issue);
78+
assert.lengthOf(selectIssues, 1);
79+
const selectIssue = selectIssues[0];
80+
81+
assert.strictEqual(selectIssue.getCategory(), IssuesManager.Issue.IssueCategory.OTHER);
82+
assert.deepEqual(selectIssue.details(), issueDetails);
83+
assert.strictEqual(selectIssue.getKind(), IssuesManager.Issue.IssueKind.PAGE_ERROR);
84+
assert.isNotNull(selectIssue.getDescription());
85+
});
86+
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2025 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 type * as SDK from '../../core/sdk/sdk.js';
6+
import * as Protocol from '../../generated/protocol.js';
7+
8+
import {Issue, IssueCategory, IssueKind} from './Issue.js';
9+
import {
10+
type LazyMarkdownIssueDescription,
11+
type MarkdownIssueDescription,
12+
resolveLazyDescription
13+
} from './MarkdownIssueDescription.js';
14+
15+
export class SelectElementAccessibilityIssue extends Issue {
16+
private issueDetails: Protocol.Audits.SelectElementAccessibilityIssueDetails;
17+
18+
constructor(
19+
issueDetails: Protocol.Audits.SelectElementAccessibilityIssueDetails, issuesModel: SDK.IssuesModel.IssuesModel,
20+
issueId?: Protocol.Audits.IssueId) {
21+
const issueCode = [
22+
Protocol.Audits.InspectorIssueCode.SelectElementAccessibilityIssue,
23+
issueDetails.selectElementAccessibilityIssueReason,
24+
].join('::');
25+
super(issueCode, issuesModel, issueId);
26+
this.issueDetails = issueDetails;
27+
}
28+
29+
primaryKey(): string {
30+
return JSON.stringify(this.issueDetails);
31+
}
32+
33+
getDescription(): MarkdownIssueDescription|null {
34+
if (this.issueDetails.hasDisallowedAttributes &&
35+
(this.issueDetails.selectElementAccessibilityIssueReason !==
36+
Protocol.Audits.SelectElementAccessibilityIssueReason.InteractiveContentOptionChild)) {
37+
return {
38+
file: 'selectElementAccessibilityInteractiveContentAttributesSelectDescendant.md',
39+
links: [],
40+
};
41+
}
42+
const description = issueDescriptions.get(this.issueDetails.selectElementAccessibilityIssueReason);
43+
if (!description) {
44+
return null;
45+
}
46+
return resolveLazyDescription(description);
47+
}
48+
49+
getKind(): IssueKind {
50+
return IssueKind.PAGE_ERROR;
51+
}
52+
53+
getCategory(): IssueCategory {
54+
return IssueCategory.OTHER;
55+
}
56+
57+
details(): Protocol.Audits.SelectElementAccessibilityIssueDetails {
58+
return this.issueDetails;
59+
}
60+
61+
static fromInspectorIssue(issuesModel: SDK.IssuesModel.IssuesModel, inspectorIssue: Protocol.Audits.InspectorIssue):
62+
SelectElementAccessibilityIssue[] {
63+
const selectElementAccessibilityIssueDetails = inspectorIssue.details.selectElementAccessibilityIssueDetails;
64+
if (!selectElementAccessibilityIssueDetails) {
65+
console.warn('Select Element Accessibility issue without details received.');
66+
return [];
67+
}
68+
return [new SelectElementAccessibilityIssue(
69+
selectElementAccessibilityIssueDetails, issuesModel, inspectorIssue.issueId)];
70+
}
71+
}
72+
73+
const issueDescriptions: Map<Protocol.Audits.SelectElementAccessibilityIssueReason, LazyMarkdownIssueDescription> =
74+
new Map([
75+
[
76+
Protocol.Audits.SelectElementAccessibilityIssueReason.DisallowedSelectChild,
77+
{
78+
file: 'selectElementAccessibilityDisallowedSelectChild.md',
79+
links: [],
80+
},
81+
],
82+
[
83+
Protocol.Audits.SelectElementAccessibilityIssueReason.DisallowedOptGroupChild,
84+
{
85+
file: 'selectElementAccessibilityDisallowedOptGroupChild.md',
86+
links: [],
87+
},
88+
],
89+
[
90+
Protocol.Audits.SelectElementAccessibilityIssueReason.NonPhrasingContentOptionChild,
91+
{
92+
file: 'selectElementAccessibilityNonPhrasingContentOptionChild.md',
93+
links: [],
94+
},
95+
],
96+
[
97+
Protocol.Audits.SelectElementAccessibilityIssueReason.InteractiveContentOptionChild,
98+
{
99+
file: 'selectElementAccessibilityInteractiveContentOptionChild.md',
100+
links: [],
101+
},
102+
],
103+
[
104+
Protocol.Audits.SelectElementAccessibilityIssueReason.InteractiveContentLegendChild,
105+
{
106+
file: 'selectElementAccessibilityInteractiveContentLegendChild.md',
107+
links: [],
108+
},
109+
],
110+
]);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Invalid element or text node within <optgroup>
2+
3+
An element which is not allowed in the content model of the `<optgroup>` element was found within an `<optgroup>` element. These elements will not consistently be accessible to people navigating by keyboard or using assistive technology.
4+
5+
If using disallowed elements for layout structure and styling, consider using the allowed `<div>` element instead.
6+
7+
Any text existing within the `<optgroup>` element should either be removed or relocated to a valid element that allows text descendants, e.g., the `<legend>` or `<option>` elements.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Invalid element or text node within <select>
2+
3+
An element which is not allowed in the content model of the `<select>` element was found within a `<select>` element. These elements will not consistently be accessible to people navigating by keyboard or using assistive technology.
4+
5+
If using disallowed elements for layout structure and styling, consider using the allowed `<div>` element instead.
6+
7+
Any text existing within the `<select>` element should either be removed or relocated to a valid element that allows text descendants, e.g., an `<optgroup>` with a `<legend>` element or `<option>` elements.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Element with invalid attributes within a <select> element
2+
3+
An element with `contenteditable` or `tabindex` attributes is not an allowed child of a `<select>` element. It will not consistently be accessible to people navigating by keyboard or using assistive technology.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Interactive element inside of a <legend> element
2+
3+
An interactive element which is not allowed in the content model of the `<legend>` element was found within a `<legend>` element. Interactive elements are not allowed children of a `<legend>` element when used within an `<optgroup>` element. These elements will not consistently be accessible to people navigating by keyboard or using assistive technology.

0 commit comments

Comments
 (0)