@usefragments/core 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-DGHZQTLH.js → chunk-XN3LSDPY.js} +3 -2
- package/dist/chunk-XN3LSDPY.js.map +1 -0
- package/dist/codes/index.js +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-eEzCyfes.d.ts → governance-B5WWcmFY.d.ts} +19 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +110 -53
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/governance-integrity.test.ts +93 -2
- package/src/governance-integrity.ts +46 -2
- package/src/governance.ts +15 -3
- package/src/rules/a11y-required-accessible-name.ts +45 -2
- package/src/rules/index.ts +4 -0
- package/src/rules/rules.test.ts +72 -0
- package/dist/chunk-DGHZQTLH.js.map +0 -1
package/dist/react-types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ComponentType, ReactNode, JSX } from 'react';
|
|
2
|
-
export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-
|
|
2
|
+
export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-B5WWcmFY.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
import './topology/index.js';
|
|
5
5
|
import './types-xJ2xyp_G.js';
|
package/dist/schemas/index.js
CHANGED
package/dist/test-utils.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -118,6 +118,90 @@ describe("inert config diagnostics", () => {
|
|
|
118
118
|
expect(detectUnconsumedConfigKeys(authored, parsed)).toEqual([]);
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
it("accepts valid options declared by a registered rule schema", () => {
|
|
122
|
+
const options = { ignoreParents: ["FormControlLabel"], ignoreSpread: true };
|
|
123
|
+
const authored = {
|
|
124
|
+
govern: {
|
|
125
|
+
rules: {
|
|
126
|
+
"a11y/required-accessible-name": { enabled: true, options },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
const parsed = fragmentsConfigSchema.parse(authored);
|
|
131
|
+
|
|
132
|
+
expect(detectUnconsumedConfigKeys(authored, parsed)).toEqual([]);
|
|
133
|
+
expect(
|
|
134
|
+
(
|
|
135
|
+
parsed.govern?.rules?.["a11y/required-accessible-name"] as {
|
|
136
|
+
options?: unknown;
|
|
137
|
+
}
|
|
138
|
+
).options
|
|
139
|
+
).toEqual(options);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("diagnoses invalid options against the registered rule schema", () => {
|
|
143
|
+
const authored = {
|
|
144
|
+
govern: {
|
|
145
|
+
rules: {
|
|
146
|
+
"a11y/required-accessible-name": {
|
|
147
|
+
enabled: true,
|
|
148
|
+
options: { ignoreSpread: "yes" },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const parsed = fragmentsConfigSchema.parse(authored);
|
|
154
|
+
|
|
155
|
+
expect(detectUnconsumedConfigKeys(authored, parsed)).toEqual([
|
|
156
|
+
expect.objectContaining({
|
|
157
|
+
code: "FUI9004",
|
|
158
|
+
path: "govern.rules.a11y/required-accessible-name.options",
|
|
159
|
+
message: expect.stringMatching(
|
|
160
|
+
/a11y\/required-accessible-name.*ignoreSpread.*Expected boolean/
|
|
161
|
+
),
|
|
162
|
+
}),
|
|
163
|
+
]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("diagnoses a typo'd option key instead of letting it silently no-op", () => {
|
|
167
|
+
const authored = {
|
|
168
|
+
govern: {
|
|
169
|
+
rules: {
|
|
170
|
+
"a11y/required-accessible-name": {
|
|
171
|
+
enabled: true,
|
|
172
|
+
options: { ignoredParents: ["FormControlLabel"] },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
const parsed = fragmentsConfigSchema.parse(authored);
|
|
178
|
+
|
|
179
|
+
expect(detectUnconsumedConfigKeys(authored, parsed)).toEqual([
|
|
180
|
+
expect.objectContaining({
|
|
181
|
+
code: "FUI9004",
|
|
182
|
+
path: "govern.rules.a11y/required-accessible-name.options",
|
|
183
|
+
message: expect.stringMatching(/ignoredParents/),
|
|
184
|
+
}),
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("leaves options on rules without a schema untouched", () => {
|
|
189
|
+
const options = { projectSpecificSetting: "unchanged" };
|
|
190
|
+
const authored = {
|
|
191
|
+
govern: {
|
|
192
|
+
rules: {
|
|
193
|
+
"styles/no-raw-color": { enabled: true, options },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
const parsed = fragmentsConfigSchema.parse(authored);
|
|
198
|
+
|
|
199
|
+
expect(detectUnconsumedConfigKeys(authored, parsed)).toEqual([]);
|
|
200
|
+
expect(
|
|
201
|
+
(parsed.govern?.rules?.["styles/no-raw-color"] as { options?: unknown }).options
|
|
202
|
+
).toEqual(options);
|
|
203
|
+
});
|
|
204
|
+
|
|
121
205
|
it("reports an authored spacing scale that no effective property policy references", () => {
|
|
122
206
|
const declared: GovernanceConfig = {
|
|
123
207
|
scales: {
|
|
@@ -167,8 +251,15 @@ describe("inert config diagnostics", () => {
|
|
|
167
251
|
expect(detectOrphanGovernanceScales(policy, policy)).toEqual([]);
|
|
168
252
|
});
|
|
169
253
|
|
|
170
|
-
it("accepts
|
|
171
|
-
const authored = {
|
|
254
|
+
it("accepts governance CI gates as consumed keys", () => {
|
|
255
|
+
const authored = {
|
|
256
|
+
govern: {
|
|
257
|
+
ci: {
|
|
258
|
+
failOnInert: true,
|
|
259
|
+
failOnAdoptionRegression: true,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
};
|
|
172
263
|
|
|
173
264
|
expect(detectUnconsumedConfigKeys(authored, fragmentsConfigSchema.parse(authored))).toEqual([]);
|
|
174
265
|
});
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* calling in.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { ZodObject } from "zod";
|
|
21
|
+
|
|
20
22
|
import type { CanonicalSource, GovernanceConfig, GovernanceSeverity } from "./governance.js";
|
|
21
23
|
import {
|
|
22
24
|
normalizePolicyExcludes,
|
|
@@ -25,7 +27,8 @@ import {
|
|
|
25
27
|
} from "./policy-exclude.js";
|
|
26
28
|
import { compileGlobalGovernanceFacts } from "./facts/index.js";
|
|
27
29
|
import type { FactConflict } from "./facts/index.js";
|
|
28
|
-
import {
|
|
30
|
+
import { RULES } from "./rules/index.js";
|
|
31
|
+
import { RULE_FAMILY_IDS, ruleFamilyMembers } from "./rules/families.js";
|
|
29
32
|
import { FRAGMENTS_INTERNAL_RULE_IDS, RULE_TIER } from "./rules/tiers.js";
|
|
30
33
|
import { BLOCKING_RULE_ALLOWLIST } from "./rules/emit-gate.js";
|
|
31
34
|
|
|
@@ -175,6 +178,23 @@ function dedupe(values: string[]): string[] {
|
|
|
175
178
|
|
|
176
179
|
const CONSUMED_RULE_IDS = new Set(Object.keys(RULE_TIER));
|
|
177
180
|
const RECOGNIZED_RULE_IDS = new Set([...CONSUMED_RULE_IDS, ...RULE_FAMILY_IDS]);
|
|
181
|
+
// Reporter-strict, executor-lenient: unknown option keys are diagnosed here as
|
|
182
|
+
// the inert-config class FUI9004 exists to name, while rules keep their own
|
|
183
|
+
// tolerant parse (unknown keys stripped, valid keys still honored).
|
|
184
|
+
const RULE_OPTIONS_SCHEMAS = new Map(
|
|
185
|
+
RULES.flatMap((rule) =>
|
|
186
|
+
rule.optionsSchema
|
|
187
|
+
? [
|
|
188
|
+
[
|
|
189
|
+
rule.id,
|
|
190
|
+
rule.optionsSchema instanceof ZodObject
|
|
191
|
+
? rule.optionsSchema.strict()
|
|
192
|
+
: rule.optionsSchema,
|
|
193
|
+
] as const,
|
|
194
|
+
]
|
|
195
|
+
: []
|
|
196
|
+
)
|
|
197
|
+
);
|
|
178
198
|
// `exclude` is consumed by the scan's finding-override pass (it scopes the rule off
|
|
179
199
|
// the matching paths and reports each drop in the ignored accounting). It was absent
|
|
180
200
|
// from this allow-set, so a working key was diagnosed as inert — the diagnostic was
|
|
@@ -219,7 +239,7 @@ const PASSTHROUGH_KEYS = [
|
|
|
219
239
|
},
|
|
220
240
|
{
|
|
221
241
|
path: ["govern", "ci"],
|
|
222
|
-
keys: ["failOnWarnings", "failOnInert"],
|
|
242
|
+
keys: ["failOnWarnings", "failOnInert", "failOnAdoptionRegression"],
|
|
223
243
|
},
|
|
224
244
|
] as const;
|
|
225
245
|
|
|
@@ -546,6 +566,30 @@ function collectRuleConfigKeys(
|
|
|
546
566
|
diagnostics.push(unconsumedKeyDiagnostic([...path, ruleId, key]));
|
|
547
567
|
}
|
|
548
568
|
}
|
|
569
|
+
if (config["options"] === undefined) continue;
|
|
570
|
+
|
|
571
|
+
const invalidOptions = ruleFamilyMembers(ruleId).flatMap((memberRuleId) => {
|
|
572
|
+
const schema = RULE_OPTIONS_SCHEMAS.get(memberRuleId);
|
|
573
|
+
if (!schema) return [];
|
|
574
|
+
const parsed = schema.safeParse(config["options"]);
|
|
575
|
+
if (parsed.success) return [];
|
|
576
|
+
const problem = parsed.error.errors
|
|
577
|
+
.map((issue) => `${issue.path.join(".") || "options"}: ${issue.message}`)
|
|
578
|
+
.join("; ");
|
|
579
|
+
return [memberRuleId === ruleId ? problem : `${memberRuleId}: ${problem}`];
|
|
580
|
+
});
|
|
581
|
+
if (invalidOptions.length === 0) continue;
|
|
582
|
+
|
|
583
|
+
const diagnosticPath = configPath([...path, ruleId, "options"]);
|
|
584
|
+
diagnostics.push({
|
|
585
|
+
code: "FUI9004",
|
|
586
|
+
kind: "unconsumed-key",
|
|
587
|
+
severity: "warn",
|
|
588
|
+
path: diagnosticPath,
|
|
589
|
+
message:
|
|
590
|
+
`${diagnosticPath} is invalid for rule \`${ruleId}\`: ${invalidOptions.join("; ")}. ` +
|
|
591
|
+
"Fix the option values or remove the options block.",
|
|
592
|
+
});
|
|
549
593
|
}
|
|
550
594
|
}
|
|
551
595
|
|
package/src/governance.ts
CHANGED
|
@@ -355,6 +355,7 @@ export const governanceConfigSchema = z
|
|
|
355
355
|
.object({
|
|
356
356
|
failOnWarnings: z.boolean().optional(),
|
|
357
357
|
failOnInert: z.boolean().optional(),
|
|
358
|
+
failOnAdoptionRegression: z.boolean().optional(),
|
|
358
359
|
})
|
|
359
360
|
.passthrough()
|
|
360
361
|
.optional(),
|
|
@@ -443,9 +444,13 @@ export interface GovernanceConfig {
|
|
|
443
444
|
overrides?: ComponentPolicyOverride[];
|
|
444
445
|
|
|
445
446
|
/**
|
|
446
|
-
* Governance CI rendering options
|
|
447
|
-
*
|
|
448
|
-
*
|
|
447
|
+
* Governance CI rendering options. Under `--ci`, `failOnWarnings` defaults to `true`
|
|
448
|
+
* when absent; set it to `false` to keep warning findings reported but advisory.
|
|
449
|
+
* `failOnInert` is opt-in and makes inert-config diagnostics (FUI9004-FUI9008) fail
|
|
450
|
+
* the verdict; `--allow-inert` bypasses the inert gates.
|
|
451
|
+
* `failOnAdoptionRegression` is opt-in and makes `check --ci` compare the
|
|
452
|
+
* current component-identity adoption percentage with the committed
|
|
453
|
+
* `.fragments/adoption-baseline.json` floor.
|
|
449
454
|
*/
|
|
450
455
|
ci?: {
|
|
451
456
|
failOnWarnings?: boolean;
|
|
@@ -456,6 +461,13 @@ export interface GovernanceConfig {
|
|
|
456
461
|
* bypasses it, like the governance-inert gate.
|
|
457
462
|
*/
|
|
458
463
|
failOnInert?: boolean;
|
|
464
|
+
/**
|
|
465
|
+
* Fail `check --ci` when measurable component-identity adoption falls below
|
|
466
|
+
* the committed local floor. Update the floor through `check
|
|
467
|
+
* --update-baseline`; accepting a lower floor additionally requires
|
|
468
|
+
* `--allow-regression`.
|
|
469
|
+
*/
|
|
470
|
+
failOnAdoptionRegression?: boolean;
|
|
459
471
|
[key: string]: unknown;
|
|
460
472
|
};
|
|
461
473
|
[key: string]: unknown;
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
UsageNodeFact,
|
|
25
25
|
UsagePropResolvedFact,
|
|
26
26
|
} from "../facts/index.js";
|
|
27
|
+
import { z } from "zod";
|
|
27
28
|
|
|
28
29
|
import { isAriaHidden } from "./a11y-utils.js";
|
|
29
30
|
import { makeFinding } from "./finding.js";
|
|
@@ -31,7 +32,16 @@ import type { Finding } from "./types.js";
|
|
|
31
32
|
import { indexPropsByNodeId, indexTextChildrenByNodeId } from "./utils.js";
|
|
32
33
|
|
|
33
34
|
export const RULE_ID = "a11y/required-accessible-name";
|
|
34
|
-
export const RULE_VERSION = "
|
|
35
|
+
export const RULE_VERSION = "3";
|
|
36
|
+
|
|
37
|
+
export const a11yRequiredAccessibleNameOptionsSchema = z.object({
|
|
38
|
+
ignoreParents: z.array(z.string().min(1)).optional(),
|
|
39
|
+
ignoreSpread: z.boolean().optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export type A11yRequiredAccessibleNameOptions = z.infer<
|
|
43
|
+
typeof a11yRequiredAccessibleNameOptionsSchema
|
|
44
|
+
>;
|
|
35
45
|
|
|
36
46
|
const NAME_SOURCE_PROPS = new Set(["aria-label", "aria-labelledby", "title"]);
|
|
37
47
|
const COMPONENT_NAME_SOURCE_PROPS = new Set([...NAME_SOURCE_PROPS, "label"]);
|
|
@@ -57,7 +67,12 @@ export function ruleA11yRequiredAccessibleName(ix: FactIndex): Finding[] {
|
|
|
57
67
|
ix.byKind("usage_child_content").map((content) => [content.nodeId, content])
|
|
58
68
|
);
|
|
59
69
|
const globalPolicy = ix.policy.ruleConfig(RULE_ID);
|
|
70
|
+
const parsedOptions = a11yRequiredAccessibleNameOptionsSchema.safeParse(globalPolicy?.options);
|
|
71
|
+
const options: A11yRequiredAccessibleNameOptions = parsedOptions.success
|
|
72
|
+
? parsedOptions.data
|
|
73
|
+
: {};
|
|
60
74
|
const usageByNode = new Map(ix.byKind("usage_component").map((usage) => [usage.nodeId, usage]));
|
|
75
|
+
const usageNodes = ix.byKind("usage_node");
|
|
61
76
|
const definitions = new Map(
|
|
62
77
|
ix.byKind("component_definition").map((definition) => [definition.componentKey, definition])
|
|
63
78
|
);
|
|
@@ -73,6 +88,7 @@ export function ruleA11yRequiredAccessibleName(ix: FactIndex): Finding[] {
|
|
|
73
88
|
globalPolicy?.enabled === true &&
|
|
74
89
|
requiresAccessibleName(node, usageComponent, definitions, props);
|
|
75
90
|
if (!componentPolicy && !genericControl) continue;
|
|
91
|
+
if (isInsideIgnoredAttributeParent(node, usageNodes, options.ignoreParents)) continue;
|
|
76
92
|
|
|
77
93
|
if (
|
|
78
94
|
hasAccessibleName(
|
|
@@ -86,11 +102,16 @@ export function ruleA11yRequiredAccessibleName(ix: FactIndex): Finding[] {
|
|
|
86
102
|
continue;
|
|
87
103
|
}
|
|
88
104
|
|
|
105
|
+
const hasSpreadProps = props.some((prop) => prop.resolution === "spread");
|
|
106
|
+
if (hasSpreadProps && options.ignoreSpread === true) continue;
|
|
107
|
+
|
|
89
108
|
findings.push(
|
|
90
109
|
makeFinding({
|
|
91
110
|
ruleId: RULE_ID,
|
|
92
111
|
ruleVersion: RULE_VERSION,
|
|
93
|
-
severity:
|
|
112
|
+
severity: hasSpreadProps
|
|
113
|
+
? "minor"
|
|
114
|
+
: (componentPolicy?.severity ?? globalPolicy?.severity ?? "warn"),
|
|
94
115
|
message: `<${node.element}> has no accessible name. ${
|
|
95
116
|
componentPolicy?.because ??
|
|
96
117
|
"Add visible text, aria-label, aria-labelledby, or a component label."
|
|
@@ -119,6 +140,28 @@ export function ruleA11yRequiredAccessibleName(ix: FactIndex): Finding[] {
|
|
|
119
140
|
return findings;
|
|
120
141
|
}
|
|
121
142
|
|
|
143
|
+
function isInsideIgnoredAttributeParent(
|
|
144
|
+
node: UsageNodeFact,
|
|
145
|
+
usageNodes: readonly UsageNodeFact[],
|
|
146
|
+
ignoreParents: readonly string[] | undefined
|
|
147
|
+
): boolean {
|
|
148
|
+
if (!ignoreParents?.length || !node.nodePath.includes("/attr:")) return false;
|
|
149
|
+
const ignored = new Set(ignoreParents);
|
|
150
|
+
const segments = node.nodePath.split("/");
|
|
151
|
+
const ancestorPaths = new Set<string>();
|
|
152
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
153
|
+
if (segments[index].startsWith("attr:")) {
|
|
154
|
+
ancestorPaths.add(segments.slice(0, index).join("/"));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return usageNodes.some(
|
|
158
|
+
(candidate) =>
|
|
159
|
+
candidate.file === node.file &&
|
|
160
|
+
ancestorPaths.has(candidate.nodePath) &&
|
|
161
|
+
ignored.has(candidate.element)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
122
165
|
function hasAccessibleName(
|
|
123
166
|
props: UsagePropResolvedFact[],
|
|
124
167
|
textChildren: ReadonlyArray<{ text: string }>,
|
package/src/rules/index.ts
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { FactIndex } from "../facts/index.js";
|
|
11
|
+
import type { ZodTypeAny } from "zod";
|
|
11
12
|
|
|
12
13
|
import {
|
|
14
|
+
a11yRequiredAccessibleNameOptionsSchema,
|
|
13
15
|
RULE_VERSION as A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION,
|
|
14
16
|
ruleA11yRequiredAccessibleName,
|
|
15
17
|
} from "./a11y-required-accessible-name.js";
|
|
@@ -44,6 +46,7 @@ export interface Rule {
|
|
|
44
46
|
id: string;
|
|
45
47
|
version: string;
|
|
46
48
|
run: RuleFn;
|
|
49
|
+
optionsSchema?: ZodTypeAny;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
export const RULES: readonly Rule[] = [
|
|
@@ -156,6 +159,7 @@ export const RULES: readonly Rule[] = [
|
|
|
156
159
|
id: "a11y/required-accessible-name",
|
|
157
160
|
version: A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION,
|
|
158
161
|
run: ruleA11yRequiredAccessibleName,
|
|
162
|
+
optionsSchema: a11yRequiredAccessibleNameOptionsSchema,
|
|
159
163
|
},
|
|
160
164
|
{
|
|
161
165
|
id: "a11y/standard",
|
package/src/rules/rules.test.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
runRules,
|
|
37
37
|
} from "../index.js";
|
|
38
38
|
import type { CanonicalBridgeV1, Fact, FactId } from "../index.js";
|
|
39
|
+
import { makeGovernanceRuleConfigFact } from "../facts/index.js";
|
|
39
40
|
import { RULE_VERSION as A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION } from "./a11y-required-accessible-name.js";
|
|
40
41
|
import { makeFinding } from "./finding.js";
|
|
41
42
|
|
|
@@ -1425,6 +1426,77 @@ describe("a11y/required-accessible-name", () => {
|
|
|
1425
1426
|
expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(0);
|
|
1426
1427
|
});
|
|
1427
1428
|
|
|
1429
|
+
it("ignores controls rendered through an ignored same-file attribute parent", () => {
|
|
1430
|
+
const ix = emptyIndexWithGovernance();
|
|
1431
|
+
ix.add(
|
|
1432
|
+
makeGovernanceRuleConfigFact({
|
|
1433
|
+
ruleId: "a11y/required-accessible-name",
|
|
1434
|
+
enabled: true,
|
|
1435
|
+
options: { ignoreParents: ["FormControlLabel"] },
|
|
1436
|
+
})
|
|
1437
|
+
);
|
|
1438
|
+
ix.add(
|
|
1439
|
+
makeUsageNodeFact({
|
|
1440
|
+
file: "apps/x.tsx",
|
|
1441
|
+
nodePath: "0:0",
|
|
1442
|
+
element: "FormControlLabel",
|
|
1443
|
+
location: { file: "apps/x.tsx", line: 1, column: 0 },
|
|
1444
|
+
})
|
|
1445
|
+
);
|
|
1446
|
+
addButtonUsage(ix, {
|
|
1447
|
+
file: "apps/x.tsx",
|
|
1448
|
+
nodePath: "0:0/attr:control/0:0",
|
|
1449
|
+
line: 1,
|
|
1450
|
+
column: 28,
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(0);
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
it("demotes an unnamed component with spread props to minor", () => {
|
|
1457
|
+
const ix = emptyIndexWithGovernance();
|
|
1458
|
+
addButtonUsage(ix, {
|
|
1459
|
+
file: "apps/x.tsx",
|
|
1460
|
+
nodePath: "0:0",
|
|
1461
|
+
line: 1,
|
|
1462
|
+
column: 0,
|
|
1463
|
+
props: [{ prop: "...props", resolution: "spread" }],
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
expect(ruleA11yRequiredAccessibleName(ix)).toMatchObject([{ severity: "minor" }]);
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
it("skips an unnamed component with spread props when ignoreSpread is enabled", () => {
|
|
1470
|
+
const ix = emptyIndexWithGovernance();
|
|
1471
|
+
ix.add(
|
|
1472
|
+
makeGovernanceRuleConfigFact({
|
|
1473
|
+
ruleId: "a11y/required-accessible-name",
|
|
1474
|
+
enabled: true,
|
|
1475
|
+
options: { ignoreSpread: true },
|
|
1476
|
+
})
|
|
1477
|
+
);
|
|
1478
|
+
addButtonUsage(ix, {
|
|
1479
|
+
file: "apps/x.tsx",
|
|
1480
|
+
nodePath: "0:0",
|
|
1481
|
+
line: 1,
|
|
1482
|
+
column: 0,
|
|
1483
|
+
props: [{ prop: "...props", resolution: "spread" }],
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(0);
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
it("registers ignoreParents and ignoreSpread as supported rule options", () => {
|
|
1490
|
+
const registered = RULES.find((rule) => rule.id === "a11y/required-accessible-name");
|
|
1491
|
+
|
|
1492
|
+
expect(
|
|
1493
|
+
registered?.optionsSchema?.safeParse({
|
|
1494
|
+
ignoreParents: ["FormControlLabel"],
|
|
1495
|
+
ignoreSpread: true,
|
|
1496
|
+
}).success
|
|
1497
|
+
).toBe(true);
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1428
1500
|
it("keeps the registry version in lockstep with the emitted finding version", () => {
|
|
1429
1501
|
const registered = RULES.find((rule) => rule.id === "a11y/required-accessible-name");
|
|
1430
1502
|
expect(registered?.version).toBe(A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION);
|