diff --git a/README.md b/README.md
index 75cdd6b..c73245e 100644
--- a/README.md
+++ b/README.md
@@ -178,7 +178,7 @@ Any use of third-party trademarks or logos are subject to those third-party's po
| [avoid-using-aria-describedby-for-primary-labelling](docs/rules/avoid-using-aria-describedby-for-primary-labelling.md) | aria-describedby provides additional context and is not meant for primary labeling. | β
| | |
| [badge-needs-accessible-name](docs/rules/badge-needs-accessible-name.md) | | β
| | π§ |
| [breadcrumb-needs-labelling](docs/rules/breadcrumb-needs-labelling.md) | All interactive elements must have an accessible name | β
| | |
-| [card-needs-accessible-name](docs/rules/card-needs-accessible-name.md) | Accessibility: Interactive Card must have an accessible name via aria-label, aria-labelledby, etc. | β
| | |
+| [card-needs-accessible-name](docs/rules/card-needs-accessible-name.md) | Accessibility: Interactive Card must have an accessible name via aria-label, aria-labelledby, etc. | β
| | π§ |
| [checkbox-needs-labelling](docs/rules/checkbox-needs-labelling.md) | Accessibility: Checkbox without label must have an accessible and visual label: aria-labelledby | β
| | |
| [colorswatch-needs-labelling](docs/rules/colorswatch-needs-labelling.md) | Accessibility: ColorSwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc.. | β
| | |
| [combobox-needs-labelling](docs/rules/combobox-needs-labelling.md) | All interactive elements must have an accessible name | β
| | |
@@ -191,24 +191,24 @@ Any use of third-party trademarks or logos are subject to those third-party's po
| [emptyswatch-needs-labelling](docs/rules/emptyswatch-needs-labelling.md) | Accessibility: EmptySwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc.. | β
| | |
| [field-needs-labelling](docs/rules/field-needs-labelling.md) | Accessibility: Field must have label | β
| | |
| [image-button-missing-aria](docs/rules/image-button-missing-aria.md) | Accessibility: Image buttons must have accessible labelling: title, aria-label, aria-labelledby, aria-describedby | β
| | |
-| [image-needs-alt](docs/rules/image-needs-alt.md) | Accessibility: Image must have alt attribute with a meaningful description of the image. If the image is decorative, use alt="". | β
| | |
+| [image-needs-alt](docs/rules/image-needs-alt.md) | Accessibility: Image must have alt attribute with a meaningful description of the image. If the image is decorative, use alt="". | β
| | π§ |
| [imageswatch-needs-labelling](docs/rules/imageswatch-needs-labelling.md) | Accessibility: ImageSwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc.. | β
| | |
-| [infolabel-needs-labelling](docs/rules/infolabel-needs-labelling.md) | Accessibility: InfoLabel must have an accessible name via aria-label, text content, aria-labelledby, etc. | β
| | |
+| [infolabel-needs-labelling](docs/rules/infolabel-needs-labelling.md) | Accessibility: InfoLabel must have an accessible name via aria-label, text content, aria-labelledby, etc. | β
| | π§ |
| [input-components-require-accessible-name](docs/rules/input-components-require-accessible-name.md) | Accessibility: Input fields must have accessible labelling: aria-label, aria-labelledby or an associated label | β
| | |
| [link-missing-labelling](docs/rules/link-missing-labelling.md) | Accessibility: Image links must have an accessible name. Add either text content, labelling to the image or labelling to the link itself. | β
| | π§ |
-| [menu-button-needs-labelling](docs/rules/menu-button-needs-labelling.md) | Accessibility: MenuButton must have an accessible name via aria-label, text content, aria-labelledby, etc. | β
| | |
+| [menu-button-needs-labelling](docs/rules/menu-button-needs-labelling.md) | Accessibility: MenuButton must have an accessible name via aria-label, text content, aria-labelledby, etc. | β
| | π§ |
| [menu-item-needs-labelling](docs/rules/menu-item-needs-labelling.md) | Accessibility: MenuItem without label must have an accessible and visual label: aria-labelledby | β
| | |
| [no-empty-buttons](docs/rules/no-empty-buttons.md) | Accessibility: Button, ToggleButton, SplitButton, MenuButton, CompoundButton must either text content or icon or child component | β
| | |
| [no-empty-components](docs/rules/no-empty-components.md) | FluentUI components should not be empty | β
| | |
| [prefer-aria-over-title-attribute](docs/rules/prefer-aria-over-title-attribute.md) | The title attribute is not consistently read by screen readers, and its behavior can vary depending on the screen reader and the user's settings. | | β
| π§ |
| [prefer-disabledfocusable-over-disabled](docs/rules/prefer-disabledfocusable-over-disabled.md) | Prefer 'disabledFocusable' over 'disabled' when component has loading state to maintain keyboard navigation accessibility | | β
| π§ |
-| [progressbar-needs-labelling](docs/rules/progressbar-needs-labelling.md) | Accessibility: Progressbar must have aria-valuemin, aria-valuemax, aria-valuenow, aria-describedby and either aria-label or aria-labelledby attributes | β
| | |
+| [progressbar-needs-labelling](docs/rules/progressbar-needs-labelling.md) | Accessibility: Progressbar must have aria-valuemin, aria-valuemax, aria-valuenow, aria-describedby and either aria-label or aria-labelledby attributes | β
| | π§ |
| [radio-button-missing-label](docs/rules/radio-button-missing-label.md) | Accessibility: Radio button without label must have an accessible and visual label: aria-labelledby | β
| | |
| [radiogroup-missing-label](docs/rules/radiogroup-missing-label.md) | Accessibility: RadioGroup without label must have an accessible and visual label: aria-labelledby | β
| | |
| [rating-needs-name](docs/rules/rating-needs-name.md) | Accessibility: Ratings must have accessible labelling: name, aria-label, aria-labelledby or itemLabel which generates aria-label | β
| | |
| [spin-button-needs-labelling](docs/rules/spin-button-needs-labelling.md) | Accessibility: SpinButtons must have an accessible label | β
| | |
| [spin-button-unrecommended-labelling](docs/rules/spin-button-unrecommended-labelling.md) | Accessibility: Unrecommended accessibility labelling - SpinButton | β
| | |
-| [spinner-needs-labelling](docs/rules/spinner-needs-labelling.md) | Accessibility: Spinner must have either aria-label or label, aria-live and aria-busy attributes | β
| | |
+| [spinner-needs-labelling](docs/rules/spinner-needs-labelling.md) | Accessibility: Spinner must have either aria-label or label, aria-live and aria-busy attributes | β
| | π§ |
| [switch-needs-labelling](docs/rules/switch-needs-labelling.md) | Accessibility: Switch must have an accessible label | β
| | |
| [table-needs-labelling](docs/rules/table-needs-labelling.md) | Accessibility: Table must have proper labelling and semantic structure for screen readers | β
| | |
| [tablist-and-tabs-need-labelling](docs/rules/tablist-and-tabs-need-labelling.md) | This rule aims to ensure that Tabs with icons but no text labels have an accessible name and that Tablist is properly labeled. | β
| | |
diff --git a/docs/rules/card-needs-accessible-name.md b/docs/rules/card-needs-accessible-name.md
index 25fedaf..5b4dd6a 100644
--- a/docs/rules/card-needs-accessible-name.md
+++ b/docs/rules/card-needs-accessible-name.md
@@ -2,6 +2,8 @@
πΌ This rule is enabled in the β
`recommended` config.
+π§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+
Interactive Card components must have an accessible name for screen readers.
diff --git a/docs/rules/image-needs-alt.md b/docs/rules/image-needs-alt.md
index 452769c..d3304b7 100644
--- a/docs/rules/image-needs-alt.md
+++ b/docs/rules/image-needs-alt.md
@@ -2,6 +2,8 @@
πΌ This rule is enabled in the β
`recommended` config.
+π§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+
## Rule details
diff --git a/docs/rules/infolabel-needs-labelling.md b/docs/rules/infolabel-needs-labelling.md
index ddc01b4..1d9c845 100644
--- a/docs/rules/infolabel-needs-labelling.md
+++ b/docs/rules/infolabel-needs-labelling.md
@@ -2,6 +2,8 @@
πΌ This rule is enabled in the β
`recommended` config.
+π§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+
InfoLabel components must have accessible labelling for screen readers.
diff --git a/docs/rules/menu-button-needs-labelling.md b/docs/rules/menu-button-needs-labelling.md
index d2dca03..2950403 100644
--- a/docs/rules/menu-button-needs-labelling.md
+++ b/docs/rules/menu-button-needs-labelling.md
@@ -2,6 +2,8 @@
πΌ This rule is enabled in the β
`recommended` config.
+π§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+
MenuButton components must have accessible labelling for screen readers.
diff --git a/docs/rules/progressbar-needs-labelling.md b/docs/rules/progressbar-needs-labelling.md
index 48247ff..d41d1b7 100644
--- a/docs/rules/progressbar-needs-labelling.md
+++ b/docs/rules/progressbar-needs-labelling.md
@@ -2,6 +2,8 @@
πΌ This rule is enabled in the β
`recommended` config.
+π§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+
ProgressBar must have `max` or `aria-valuemin`, `aria-valuemax` and `aria-valuenow` attributes. It also must have an accessible `Field` parent or appropriate labelling using `aria-describedby` and `aria-label`/`aria-labelledby` .
diff --git a/docs/rules/spinner-needs-labelling.md b/docs/rules/spinner-needs-labelling.md
index 08ae0ea..34c85f9 100644
--- a/docs/rules/spinner-needs-labelling.md
+++ b/docs/rules/spinner-needs-labelling.md
@@ -2,6 +2,8 @@
πΌ This rule is enabled in the β
`recommended` config.
+π§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+
Spinner must have either aria-label or label, aria-live and aria-busy attributes.
diff --git a/lib/rules/buttons/menu-button-needs-labelling.ts b/lib/rules/buttons/menu-button-needs-labelling.ts
index e1cc841..851ae6f 100644
--- a/lib/rules/buttons/menu-button-needs-labelling.ts
+++ b/lib/rules/buttons/menu-button-needs-labelling.ts
@@ -21,6 +21,10 @@ export default ESLintUtils.RuleCreator.withoutDocs(
allowTooltipParent: true,
allowDescribedBy: false,
allowLabeledChild: true,
- allowTextContentChild: true
+ allowTextContentChild: true,
+ autoFix: {
+ strategy: "aria-label-suggestion",
+ suggestedLabel: "Open menu"
+ }
})
);
diff --git a/lib/rules/card-needs-accessible-name.ts b/lib/rules/card-needs-accessible-name.ts
index 6095db2..9f88d11 100644
--- a/lib/rules/card-needs-accessible-name.ts
+++ b/lib/rules/card-needs-accessible-name.ts
@@ -21,6 +21,10 @@ export default ESLintUtils.RuleCreator.withoutDocs(
allowTooltipParent: true,
allowDescribedBy: false,
allowLabeledChild: true,
- allowTextContentChild: true
+ allowTextContentChild: true,
+ autoFix: {
+ strategy: "aria-label-suggestion",
+ suggestedLabel: "Card"
+ }
})
);
diff --git a/lib/rules/image-needs-alt.ts b/lib/rules/image-needs-alt.ts
index 8d8f2fb..13b04ad 100644
--- a/lib/rules/image-needs-alt.ts
+++ b/lib/rules/image-needs-alt.ts
@@ -21,7 +21,12 @@ const rule = ESLintUtils.RuleCreator.withoutDocs(
allowWrappingLabel: false,
allowTooltipParent: false,
allowDescribedBy: false,
- allowLabeledChild: false
+ allowLabeledChild: false,
+ autoFix: {
+ strategy: "add-required-prop",
+ propName: "alt",
+ propValue: '""'
+ }
})
);
diff --git a/lib/rules/infolabel-needs-labelling.ts b/lib/rules/infolabel-needs-labelling.ts
index 3587e10..3721572 100644
--- a/lib/rules/infolabel-needs-labelling.ts
+++ b/lib/rules/infolabel-needs-labelling.ts
@@ -21,6 +21,10 @@ export default ESLintUtils.RuleCreator.withoutDocs(
allowTooltipParent: true,
allowDescribedBy: false,
allowLabeledChild: true,
- allowTextContentChild: true
+ allowTextContentChild: true,
+ autoFix: {
+ strategy: "aria-label-suggestion",
+ suggestedLabel: "Info"
+ }
})
);
diff --git a/lib/rules/progressbar-needs-labelling.ts b/lib/rules/progressbar-needs-labelling.ts
index 37ad191..da79347 100644
--- a/lib/rules/progressbar-needs-labelling.ts
+++ b/lib/rules/progressbar-needs-labelling.ts
@@ -28,6 +28,7 @@ const rule = ESLintUtils.RuleCreator.withoutDocs({
recommended: "strict",
url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
},
+ fixable: "code",
schema: []
},
// create (function) returns an object with methods that ESLint calls to βvisitβ nodes while traversing the abstract syntax tree
@@ -69,7 +70,39 @@ const rule = ESLintUtils.RuleCreator.withoutDocs({
// if it has no visual labelling, report error
context.report({
node,
- messageId: `noUnlabelledProgressbar`
+ messageId: `noUnlabelledProgressbar`,
+ fix(fixer) {
+ const fixes = [];
+
+ // Add aria-label if neither aria-label nor aria-labelledby exist (and no Field parent)
+ if (
+ !hasFieldParentCheck &&
+ !hasNonEmptyProp(node.attributes, "aria-label") &&
+ !hasNonEmptyProp(node.attributes, "aria-labelledby")
+ ) {
+ fixes.push(fixer.insertTextAfter(node.name, ' aria-label="Progress"'));
+ }
+
+ // Add aria-describedby if missing
+ if (!hasNonEmptyProp(node.attributes, "aria-describedby")) {
+ fixes.push(fixer.insertTextAfter(node.name, ' aria-describedby=""'));
+ }
+
+ // Add missing ARIA value attributes if max prop is not present
+ if (!hasMaxProp) {
+ if (!hasNonEmptyProp(node.attributes, "aria-valuemin")) {
+ fixes.push(fixer.insertTextAfter(node.name, ' aria-valuemin="0"'));
+ }
+ if (!hasNonEmptyProp(node.attributes, "aria-valuemax")) {
+ fixes.push(fixer.insertTextAfter(node.name, ' aria-valuemax="100"'));
+ }
+ if (!hasNonEmptyProp(node.attributes, "aria-valuenow")) {
+ fixes.push(fixer.insertTextAfter(node.name, ' aria-valuenow="0"'));
+ }
+ }
+
+ return fixes;
+ }
});
}
};
diff --git a/lib/rules/spinner-needs-labelling.ts b/lib/rules/spinner-needs-labelling.ts
index 5005cc6..c246f12 100644
--- a/lib/rules/spinner-needs-labelling.ts
+++ b/lib/rules/spinner-needs-labelling.ts
@@ -25,6 +25,7 @@ const rule = ESLintUtils.RuleCreator.withoutDocs({
recommended: "strict",
url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
},
+ fixable: "code",
schema: []
},
// create (function) returns an object with methods that ESLint calls to βvisitβ nodes while traversing the abstract syntax tree
@@ -48,7 +49,27 @@ const rule = ESLintUtils.RuleCreator.withoutDocs({
// if it has no visual labelling, report error
context.report({
node,
- messageId: `noUnlabelledSpinner`
+ messageId: `noUnlabelledSpinner`,
+ fix(fixer) {
+ const fixes = [];
+
+ // Add missing aria-label if neither label nor aria-label exist
+ if (!hasNonEmptyProp(node.attributes, "label") && !hasNonEmptyProp(node.attributes, "aria-label")) {
+ fixes.push(fixer.insertTextAfter(node.name, ' aria-label="Loading"'));
+ }
+
+ // Add missing aria-live
+ if (!hasNonEmptyProp(node.attributes, "aria-live")) {
+ fixes.push(fixer.insertTextAfter(node.name, ' aria-live="polite"'));
+ }
+
+ // Add missing aria-busy
+ if (!hasNonEmptyProp(node.attributes, "aria-busy")) {
+ fixes.push(fixer.insertTextAfter(node.name, ' aria-busy="true"'));
+ }
+
+ return fixes;
+ }
});
}
};
diff --git a/lib/util/ruleFactory.ts b/lib/util/ruleFactory.ts
index b245044..b6fdee9 100644
--- a/lib/util/ruleFactory.ts
+++ b/lib/util/ruleFactory.ts
@@ -18,6 +18,32 @@ import { hasDefinedProp } from "./hasDefinedProp";
import { hasTextContentChild } from "./hasTextContentChild";
import { hasTriggerProp } from "./hasTriggerProp";
+/**
+ * Auto-fix strategy types for accessibility rules
+ */
+export type AutoFixStrategy =
+ | "aria-label-placeholder" // Add aria-label=""
+ | "aria-label-suggestion" // Add aria-label="[Component description]"
+ | "add-required-prop" // Add specific required prop
+ | "custom"; // Custom fix logic
+
+/**
+ * Auto-fix configuration for accessibility rules
+ */
+export type AutoFixConfig = {
+ /** The auto-fix strategy to use */
+ strategy: AutoFixStrategy;
+ /** For add-required-prop: the prop name to add */
+ propName?: string;
+ /** For add-required-prop: the default prop value */
+ propValue?: string;
+ /** For aria-label-suggestion: the suggested label text */
+ suggestedLabel?: string;
+ /** For custom: custom fix function */
+ // eslint-disable-next-line no-unused-vars
+ customFix?: (opening: TSESTree.JSXOpeningElement) => string;
+};
+
/**
* Configuration options for a rule created via the `ruleFactory`
*/
@@ -52,6 +78,7 @@ export type LabeledControlConfig = {
allowTextContentChild?: boolean; // Accept text children to provide the label e.g.
triggerProp?: string; // Only apply rule when this trigger prop is present (e.g., "dismissible", "disabled")
customValidator?: Function; // Custom validation logic
+ autoFix?: AutoFixConfig; // Auto-fix configuration for the rule
};
/**
@@ -104,6 +131,41 @@ export function hasAccessibleLabel(
return false;
}
+/**
+ * Generate auto-fix for accessibility rules based on configuration
+ */
+export function generateAutoFix(opening: TSESTree.JSXOpeningElement, config: AutoFixConfig): TSESLint.ReportFixFunction | null {
+ if (!config) return null;
+
+ return (fixer: TSESLint.RuleFixer) => {
+ switch (config.strategy) {
+ case "aria-label-placeholder": {
+ return fixer.insertTextAfter(opening.name, ' aria-label=""');
+ }
+
+ case "aria-label-suggestion": {
+ const label = config.suggestedLabel || "Provide accessible name";
+ return fixer.insertTextAfter(opening.name, ` aria-label="${label}"`);
+ }
+
+ case "add-required-prop": {
+ if (!config.propName) return null;
+ const value = config.propValue || '""';
+ return fixer.insertTextAfter(opening.name, ` ${config.propName}=${value}`);
+ }
+
+ case "custom": {
+ if (!config.customFix) return null;
+ const fixText = config.customFix(opening);
+ return fixer.insertTextAfter(opening.name, fixText);
+ }
+
+ default:
+ return null;
+ }
+ };
+}
+
/**
* Factory for a minimal, strongly-configurable ESLint rule that enforces
* accessible labeling on a specific JSX element/component.
@@ -118,6 +180,7 @@ export function makeLabeledControlRule(config: LabeledControlConfig): TSESLint.R
recommended: "strict",
url: "https://www.w3.org/TR/html-aria/"
},
+ fixable: config.autoFix ? "code" : undefined,
schema: []
},
defaultOptions: [],
@@ -142,8 +205,15 @@ export function makeLabeledControlRule(config: LabeledControlConfig): TSESLint.R
: (isValid = hasAccessibleLabel(opening, node, context, config));
if (!isValid) {
+ // Generate auto-fix if configuration is provided
+ const autoFix = config.autoFix ? generateAutoFix(opening, config.autoFix) : undefined;
+
// report on the opening tag for better location
- context.report({ node: opening, messageId: config.messageId });
+ context.report({
+ node: opening,
+ messageId: config.messageId,
+ fix: autoFix
+ });
}
}
};
diff --git a/tests/lib/rules/buttons/menu-button-needs-labelling.test.ts b/tests/lib/rules/buttons/menu-button-needs-labelling.test.ts
index 113dfb0..d8d3e2f 100644
--- a/tests/lib/rules/buttons/menu-button-needs-labelling.test.ts
+++ b/tests/lib/rules/buttons/menu-button-needs-labelling.test.ts
@@ -25,19 +25,23 @@ ruleTester.run("menu-button-needs-labelling", rule as unknown as Rule.RuleModule
invalid: [
{
code: ``,
- errors: [{ messageId: "menuButtonNeedsLabelling" }]
+ errors: [{ messageId: "menuButtonNeedsLabelling" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "menuButtonNeedsLabelling" }]
+ errors: [{ messageId: "menuButtonNeedsLabelling" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "menuButtonNeedsLabelling" }]
+ errors: [{ messageId: "menuButtonNeedsLabelling" }],
+ output: ``
},
{
code: `<>>`,
- errors: [{ messageId: "menuButtonNeedsLabelling" }]
+ errors: [{ messageId: "menuButtonNeedsLabelling" }],
+ output: `<>>`
}
]
});
diff --git a/tests/lib/rules/card-needs-accessible-name.test.ts b/tests/lib/rules/card-needs-accessible-name.test.ts
index 4cddd19..d8afda7 100644
--- a/tests/lib/rules/card-needs-accessible-name.test.ts
+++ b/tests/lib/rules/card-needs-accessible-name.test.ts
@@ -23,19 +23,23 @@ ruleTester.run("card-needs-accessible-name", rule as unknown as Rule.RuleModule,
invalid: [
{
code: ``,
- errors: [{ messageId: "cardNeedsAccessibleName" }]
+ errors: [{ messageId: "cardNeedsAccessibleName" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "cardNeedsAccessibleName" }]
+ errors: [{ messageId: "cardNeedsAccessibleName" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "cardNeedsAccessibleName" }]
+ errors: [{ messageId: "cardNeedsAccessibleName" }],
+ output: ``
},
{
code: `<>>`,
- errors: [{ messageId: "cardNeedsAccessibleName" }]
+ errors: [{ messageId: "cardNeedsAccessibleName" }],
+ output: `<>>`
}
]
});
diff --git a/tests/lib/rules/image-needs-alt.test.ts b/tests/lib/rules/image-needs-alt.test.ts
index ad1f9b5..5a5401e 100644
--- a/tests/lib/rules/image-needs-alt.test.ts
+++ b/tests/lib/rules/image-needs-alt.test.ts
@@ -24,17 +24,20 @@ ruleTester.run("image-needs-alt", rule as unknown as Rule.RuleModule, {
{
// No alt attribute
code: '',
- errors: [{ messageId: "imageNeedsAlt" }]
+ errors: [{ messageId: "imageNeedsAlt" }],
+ output: ''
},
{
// Null alt attribute
code: '',
- errors: [{ messageId: "imageNeedsAlt" }]
+ errors: [{ messageId: "imageNeedsAlt" }],
+ output: ''
},
{
// Undefined alt attribute
code: '',
- errors: [{ messageId: "imageNeedsAlt" }]
+ errors: [{ messageId: "imageNeedsAlt" }],
+ output: ''
}
]
});
diff --git a/tests/lib/rules/infolabel-needs-labelling.test.ts b/tests/lib/rules/infolabel-needs-labelling.test.ts
index ac1ee2a..09877ec 100644
--- a/tests/lib/rules/infolabel-needs-labelling.test.ts
+++ b/tests/lib/rules/infolabel-needs-labelling.test.ts
@@ -27,31 +27,38 @@ ruleTester.run("infolabel-needs-labelling", rule as unknown as Rule.RuleModule,
invalid: [
{
code: ``,
- errors: [{ messageId: "infoLabelNeedsLabelling" }]
+ errors: [{ messageId: "infoLabelNeedsLabelling" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "infoLabelNeedsLabelling" }]
+ errors: [{ messageId: "infoLabelNeedsLabelling" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "infoLabelNeedsLabelling" }]
+ errors: [{ messageId: "infoLabelNeedsLabelling" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "infoLabelNeedsLabelling" }]
+ errors: [{ messageId: "infoLabelNeedsLabelling" }],
+ output: ``
},
{
code: `<>>`,
- errors: [{ messageId: "infoLabelNeedsLabelling" }]
+ errors: [{ messageId: "infoLabelNeedsLabelling" }],
+ output: `<>>`
},
{
code: `
`,
- errors: [{ messageId: "infoLabelNeedsLabelling" }]
+ errors: [{ messageId: "infoLabelNeedsLabelling" }],
+ output: `
`
},
{
code: `
`,
- errors: [{ messageId: "infoLabelNeedsLabelling" }]
+ errors: [{ messageId: "infoLabelNeedsLabelling" }],
+ output: `
`
}
]
});
diff --git a/tests/lib/rules/progressbar-needs-labelling.test.ts b/tests/lib/rules/progressbar-needs-labelling.test.ts
index a46e26c..c16a65c 100644
--- a/tests/lib/rules/progressbar-needs-labelling.test.ts
+++ b/tests/lib/rules/progressbar-needs-labelling.test.ts
@@ -59,7 +59,14 @@ ruleTester.run("progressbar-needs-labelling", rule as unknown as Rule.RuleModule
>
`,
- errors: [{ messageId: "noUnlabelledProgressbar" }]
+ errors: [{ messageId: "noUnlabelledProgressbar" }],
+ output: `
+
+ `
},
{
code: `
`,
- errors: [{ messageId: "noUnlabelledProgressbar" }]
+ errors: [{ messageId: "noUnlabelledProgressbar" }],
+ output: `
+
+ `
},
{
code: `
`,
- errors: [{ messageId: "noUnlabelledProgressbar" }]
+ errors: [{ messageId: "noUnlabelledProgressbar" }],
+ output: `
+
+ `
},
{
code: `
`,
- errors: [{ messageId: "noUnlabelledProgressbar" }]
+ errors: [{ messageId: "noUnlabelledProgressbar" }],
+ output: `
+
+ `
},
{
code: ``,
- errors: [{ messageId: "noUnlabelledProgressbar" }]
+ errors: [{ messageId: "noUnlabelledProgressbar" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "noUnlabelledProgressbar" }]
+ errors: [{ messageId: "noUnlabelledProgressbar" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "noUnlabelledProgressbar" }]
+ errors: [{ messageId: "noUnlabelledProgressbar" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "noUnlabelledProgressbar" }]
+ errors: [{ messageId: "noUnlabelledProgressbar" }],
+ output: ``
}
]
});
diff --git a/tests/lib/rules/spinner-needs-labelling.test.ts b/tests/lib/rules/spinner-needs-labelling.test.ts
index 2eeb622..6f1b316 100644
--- a/tests/lib/rules/spinner-needs-labelling.test.ts
+++ b/tests/lib/rules/spinner-needs-labelling.test.ts
@@ -23,27 +23,33 @@ ruleTester.run("spinner-needs-labelling", rule as unknown as Rule.RuleModule, {
invalid: [
{
code: ``,
- errors: [{ messageId: "noUnlabelledSpinner" }]
+ errors: [{ messageId: "noUnlabelledSpinner" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "noUnlabelledSpinner" }]
+ errors: [{ messageId: "noUnlabelledSpinner" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "noUnlabelledSpinner" }]
+ errors: [{ messageId: "noUnlabelledSpinner" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "noUnlabelledSpinner" }]
+ errors: [{ messageId: "noUnlabelledSpinner" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "noUnlabelledSpinner" }]
+ errors: [{ messageId: "noUnlabelledSpinner" }],
+ output: ``
},
{
code: ``,
- errors: [{ messageId: "noUnlabelledSpinner" }]
+ errors: [{ messageId: "noUnlabelledSpinner" }],
+ output: ``
}
]
});
diff --git a/tests/lib/rules/utils/ruleFactory.test.ts b/tests/lib/rules/utils/ruleFactory.test.ts
index 178e18a..6a83141 100644
--- a/tests/lib/rules/utils/ruleFactory.test.ts
+++ b/tests/lib/rules/utils/ruleFactory.test.ts
@@ -17,7 +17,13 @@ import {
hasAssociatedLabelViaHtmlFor,
isInsideLabelTag
} from "../../../../lib/util/labelUtils";
-import { hasAccessibleLabel, LabeledControlConfig, makeLabeledControlRule } from "../../../../lib/util/ruleFactory";
+import {
+ hasAccessibleLabel,
+ LabeledControlConfig,
+ makeLabeledControlRule,
+ generateAutoFix,
+ AutoFixConfig
+} from "../../../../lib/util/ruleFactory";
jest.mock("../../../../lib/util/hasDefinedProp", () => ({
hasDefinedProp: jest.fn()
@@ -526,3 +532,219 @@ describe("makeLabeledControlRule (RuleTester integration)", () => {
});
});
});
+
+// Tests for auto-fix functionality
+describe("generateAutoFix", () => {
+ const mockFixer = {
+ insertTextAfter: jest.fn().mockReturnValue({ type: "fix" }),
+ insertTextBefore: jest.fn().mockReturnValue({ type: "fix" }),
+ replaceText: jest.fn().mockReturnValue({ type: "fix" }),
+ replaceTextRange: jest.fn().mockReturnValue({ type: "fix" }),
+ removeRange: jest.fn().mockReturnValue({ type: "fix" })
+ } as unknown as TSESLint.RuleFixer;
+
+ const mockOpening = {
+ type: AST_NODE_TYPES.JSXOpeningElement,
+ name: {
+ type: AST_NODE_TYPES.JSXIdentifier,
+ name: "Button",
+ range: [0, 6] as [number, number],
+ loc: {} as any
+ },
+ attributes: [],
+ selfClosing: false,
+ range: [0, 8] as [number, number],
+ loc: {} as any
+ } as TSESTree.JSXOpeningElement;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("returns null when config is falsy", () => {
+ const result = generateAutoFix(mockOpening, null as any);
+ expect(result).toBeNull();
+ });
+
+ test("aria-label-placeholder strategy", () => {
+ const config: AutoFixConfig = {
+ strategy: "aria-label-placeholder"
+ };
+
+ const fixFunction = generateAutoFix(mockOpening, config);
+ expect(fixFunction).toBeDefined();
+
+ const result = fixFunction!(mockFixer);
+ expect(mockFixer.insertTextAfter).toHaveBeenCalledWith(mockOpening.name, ' aria-label=""');
+ expect(result).toEqual({ type: "fix" });
+ });
+
+ test("aria-label-suggestion strategy with default label", () => {
+ const config: AutoFixConfig = {
+ strategy: "aria-label-suggestion"
+ };
+
+ const fixFunction = generateAutoFix(mockOpening, config);
+ expect(fixFunction).toBeDefined();
+
+ const result = fixFunction!(mockFixer);
+ expect(mockFixer.insertTextAfter).toHaveBeenCalledWith(mockOpening.name, ' aria-label="Provide accessible name"');
+ expect(result).toEqual({ type: "fix" });
+ });
+
+ test("aria-label-suggestion strategy with custom label", () => {
+ const config: AutoFixConfig = {
+ strategy: "aria-label-suggestion",
+ suggestedLabel: "Custom button label"
+ };
+
+ const fixFunction = generateAutoFix(mockOpening, config);
+ expect(fixFunction).toBeDefined();
+
+ const result = fixFunction!(mockFixer);
+ expect(mockFixer.insertTextAfter).toHaveBeenCalledWith(mockOpening.name, ' aria-label="Custom button label"');
+ expect(result).toEqual({ type: "fix" });
+ });
+
+ test("add-required-prop strategy with propName only", () => {
+ const config: AutoFixConfig = {
+ strategy: "add-required-prop",
+ propName: "alt"
+ };
+
+ const fixFunction = generateAutoFix(mockOpening, config);
+ expect(fixFunction).toBeDefined();
+
+ const result = fixFunction!(mockFixer);
+ expect(mockFixer.insertTextAfter).toHaveBeenCalledWith(mockOpening.name, ' alt=""');
+ expect(result).toEqual({ type: "fix" });
+ });
+
+ test("add-required-prop strategy with propName and propValue", () => {
+ const config: AutoFixConfig = {
+ strategy: "add-required-prop",
+ propName: "role",
+ propValue: '"button"'
+ };
+
+ const fixFunction = generateAutoFix(mockOpening, config);
+ expect(fixFunction).toBeDefined();
+
+ const result = fixFunction!(mockFixer);
+ expect(mockFixer.insertTextAfter).toHaveBeenCalledWith(mockOpening.name, ' role="button"');
+ expect(result).toEqual({ type: "fix" });
+ });
+
+ test("add-required-prop strategy returns null when propName is missing", () => {
+ const config: AutoFixConfig = {
+ strategy: "add-required-prop"
+ // propName is missing
+ };
+
+ const fixFunction = generateAutoFix(mockOpening, config);
+ expect(fixFunction).toBeDefined();
+
+ const result = fixFunction!(mockFixer);
+ expect(result).toBeNull();
+ expect(mockFixer.insertTextAfter).not.toHaveBeenCalled();
+ });
+
+ test("custom strategy with customFix function", () => {
+ const mockCustomFix = jest.fn().mockReturnValue(' custom-attribute="value"');
+ const config: AutoFixConfig = {
+ strategy: "custom",
+ customFix: mockCustomFix
+ };
+
+ const fixFunction = generateAutoFix(mockOpening, config);
+ expect(fixFunction).toBeDefined();
+
+ const result = fixFunction!(mockFixer);
+ expect(mockCustomFix).toHaveBeenCalledWith(mockOpening);
+ expect(mockFixer.insertTextAfter).toHaveBeenCalledWith(mockOpening.name, ' custom-attribute="value"');
+ expect(result).toEqual({ type: "fix" });
+ });
+
+ test("custom strategy returns null when customFix is missing", () => {
+ const config: AutoFixConfig = {
+ strategy: "custom"
+ // customFix is missing
+ };
+
+ const fixFunction = generateAutoFix(mockOpening, config);
+ expect(fixFunction).toBeDefined();
+
+ const result = fixFunction!(mockFixer);
+ expect(result).toBeNull();
+ expect(mockFixer.insertTextAfter).not.toHaveBeenCalled();
+ });
+
+ test("default case returns null for unknown strategy", () => {
+ const config: AutoFixConfig = {
+ strategy: "unknown-strategy" as any
+ };
+
+ const fixFunction = generateAutoFix(mockOpening, config);
+ expect(fixFunction).toBeDefined();
+
+ const result = fixFunction!(mockFixer);
+ expect(result).toBeNull();
+ expect(mockFixer.insertTextAfter).not.toHaveBeenCalled();
+ });
+});
+
+// Tests for auto-fix integration in makeLabeledControlRule
+describe("makeLabeledControlRule with auto-fix", () => {
+ beforeEach(() => {
+ resetAllMocksToFalse();
+ });
+
+ test("generateAutoFix integration - fix function is generated correctly", () => {
+ // Test the integration by directly calling generateAutoFix with mock data
+ const mockOpening = {
+ type: AST_NODE_TYPES.JSXOpeningElement,
+ name: {
+ type: AST_NODE_TYPES.JSXIdentifier,
+ name: "Button",
+ range: [0, 6] as [number, number],
+ loc: {} as any
+ },
+ attributes: [],
+ selfClosing: false,
+ range: [0, 8] as [number, number],
+ loc: {} as any
+ } as TSESTree.JSXOpeningElement;
+
+ const config: AutoFixConfig = {
+ strategy: "aria-label-placeholder"
+ };
+
+ const fixFunction = generateAutoFix(mockOpening, config);
+ expect(fixFunction).toBeDefined();
+ expect(typeof fixFunction).toBe("function");
+
+ // Test that the fix function works when called
+ const mockFixer = {
+ insertTextAfter: jest.fn().mockReturnValue({ type: "fix" })
+ } as unknown as TSESLint.RuleFixer;
+
+ const result = fixFunction!(mockFixer);
+ expect(mockFixer.insertTextAfter).toHaveBeenCalledWith(mockOpening.name, ' aria-label=""');
+ expect(result).toEqual({ type: "fix" });
+ });
+
+ test("generateAutoFix with null config returns null", () => {
+ const mockOpening = {
+ type: AST_NODE_TYPES.JSXOpeningElement,
+ name: {
+ type: AST_NODE_TYPES.JSXIdentifier,
+ name: "Button",
+ range: [0, 6] as [number, number],
+ loc: {} as any
+ }
+ } as TSESTree.JSXOpeningElement;
+
+ const result = generateAutoFix(mockOpening, null as any);
+ expect(result).toBeNull();
+ });
+});