diff --git a/README.md b/README.md
index aecb184..23c6a89 100644
--- a/README.md
+++ b/README.md
@@ -122,7 +122,7 @@ Any use of third-party trademarks or logos are subject to those third-party's po
| [dialogbody-needs-title-content-and-actions](docs/rules/dialogbody-needs-title-content-and-actions.md) | A DialogBody should have a header(DialogTitle), content(DialogContent), and footer(DialogActions) | ✅ | | |
| [dialogsurface-needs-aria](docs/rules/dialogsurface-needs-aria.md) | DialogueSurface need accessible labelling: aria-describedby on DialogueSurface and aria-label or aria-labelledby(if DialogueTitle is missing) | ✅ | | |
| [dropdown-needs-labelling](docs/rules/dropdown-needs-labelling.md) | Accessibility: Dropdown menu must have an id and it needs to be linked via htmlFor of a Label | ✅ | | |
-| [field-needs-labelling](docs/rules/field-needs-labelling.md) | Accessibility: Field must have either label, validationMessage and hint attributes | ✅ | | |
+| [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 | ✅ | | |
| [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. | ✅ | | 🔧 |
@@ -141,6 +141,6 @@ Any use of third-party trademarks or logos are subject to those third-party's po
| [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. | ✅ | | |
| [toolbar-missing-aria](docs/rules/toolbar-missing-aria.md) | Accessibility: Toolbars need accessible labelling: aria-label or aria-labelledby | ✅ | | |
| [tooltip-not-recommended](docs/rules/tooltip-not-recommended.md) | Accessibility: Prefer text content or aria over a tooltip for these components MenuItem, SpinButton | ✅ | | |
-| [visual-label-better-than-aria-suggestion](docs/rules/visual-label-better-than-aria-suggestion.md) | Visual label is better than an aria-label | | ✅ | |
+| [visual-label-better-than-aria-suggestion](docs/rules/visual-label-better-than-aria-suggestion.md) | Visual label is better than an aria-label because sighted users can't read the aria-label text. | | ✅ | |
diff --git a/docs/rules/field-needs-labelling.md b/docs/rules/field-needs-labelling.md
index 37fb792..37332ae 100644
--- a/docs/rules/field-needs-labelling.md
+++ b/docs/rules/field-needs-labelling.md
@@ -1,10 +1,10 @@
-# Accessibility: Field must have either label, validationMessage and hint attributes (`@microsoft/fluentui-jsx-a11y/field-needs-labelling`)
+# Accessibility: Field must have label (`@microsoft/fluentui-jsx-a11y/field-needs-labelling`)
💼 This rule is enabled in the ✅ `recommended` config.
-Field must have `label` prop and either `validationMessage` or `hint` prop.
+Field must have `label` prop.
@@ -12,7 +12,6 @@ Field must have `label` prop and either `validationMessage` or `hint` prop.
- Make sure that Field component has following props:
- `label`
- - `validationMessage` or `hint`
## Rule Details
@@ -21,19 +20,13 @@ This rule aims to make Field component accessible.
Examples of **incorrect** code for this rule:
```jsx
-
+
```
```jsx
-
+
```
@@ -41,21 +34,19 @@ Examples of **incorrect** code for this rule:
Examples of **correct** code for this rule:
```jsx
-
+
+
+
+```
+
+```jsx
+
```
```jsx
-
+
```
diff --git a/docs/rules/visual-label-better-than-aria-suggestion.md b/docs/rules/visual-label-better-than-aria-suggestion.md
index 7dff8c6..6cdd715 100644
--- a/docs/rules/visual-label-better-than-aria-suggestion.md
+++ b/docs/rules/visual-label-better-than-aria-suggestion.md
@@ -1,4 +1,4 @@
-# Visual label is better than an aria-label (`@microsoft/fluentui-jsx-a11y/visual-label-better-than-aria-suggestion`)
+# Visual label is better than an aria-label because sighted users can't read the aria-label text (`@microsoft/fluentui-jsx-a11y/visual-label-better-than-aria-suggestion`)
⚠️ This rule _warns_ in the ✅ `recommended` config.
diff --git a/lib/applicableComponents/labelBasedComponents.ts b/lib/applicableComponents/labelBasedComponents.ts
new file mode 100644
index 0000000..4f847a8
--- /dev/null
+++ b/lib/applicableComponents/labelBasedComponents.ts
@@ -0,0 +1,4 @@
+const labelBasedComponents = ["Label", "label"];
+const elementsUsedAsLabels = ["div", "span", "p", "h1", "h2", "h3", "h4", "h5", "h6"];
+
+export { labelBasedComponents, elementsUsedAsLabels };
diff --git a/lib/rules/field-needs-labelling.js b/lib/rules/field-needs-labelling.ts
similarity index 73%
rename from lib/rules/field-needs-labelling.js
rename to lib/rules/field-needs-labelling.ts
index 4db33e4..cc0536b 100644
--- a/lib/rules/field-needs-labelling.js
+++ b/lib/rules/field-needs-labelling.ts
@@ -5,41 +5,41 @@
const { hasNonEmptyProp } = require("../util/hasNonEmptyProp");
const elementType = require("jsx-ast-utils").elementType;
+import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
-module.exports = {
+const rule = ESLintUtils.RuleCreator.withoutDocs({
+ defaultOptions: [],
meta: {
// possible error messages for the rule
messages: {
- noUnlabelledField: "Accessibility: Field must have either label, validationMessage and hint attributes"
+ noUnlabelledField: "Accessibility: Field must have label"
},
// "problem" means the rule is identifying code that either will cause an error or may cause a confusing behavior: https://eslint.org/docs/latest/developer-guide/working-with-rules
type: "problem",
// docs for the rule
docs: {
- description: "Accessibility: Field must have either label, validationMessage and hint attributes",
- recommended: true,
+ description: "Accessibility: Field must have label",
+ recommended: "strict",
url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
},
schema: []
},
+
// create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
create(context) {
return {
// visitor functions for different types of nodes
- JSXOpeningElement(node) {
+ JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
// if it is not a Spinner, return
if (elementType(node) !== "Field") {
return;
}
- if (
- hasNonEmptyProp(node.attributes, "label", true) &&
- (hasNonEmptyProp(node.attributes, "validationMessage", true) || hasNonEmptyProp(node.attributes, "hint", true))
- ) {
+ if (hasNonEmptyProp(node.attributes, "label")) {
return;
}
@@ -51,5 +51,6 @@ module.exports = {
}
};
}
-};
+});
+export default rule;
diff --git a/lib/rules/input-components-require-accessible-name.ts b/lib/rules/input-components-require-accessible-name.ts
index 9fa0c88..0a214d0 100644
--- a/lib/rules/input-components-require-accessible-name.ts
+++ b/lib/rules/input-components-require-accessible-name.ts
@@ -7,6 +7,7 @@ import { isInsideLabelTag, hasAssociatedLabelViaHtmlFor, hasAssociatedLabelViaAr
import { hasFieldParent } from "../util/hasFieldParent";
import { applicableComponents } from "../applicableComponents/inputBasedComponents";
import { JSXOpeningElement } from "estree-jsx";
+import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
//------------------------------------------------------------------------------
// Rule Definition
@@ -17,7 +18,7 @@ const rule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
// possible error messages for the rule
messages: {
- missingLabelOnInput: `Accessibility - input fields must have a aria label associated with it: ${applicableComponents.join(
+ missingLabelOnInput: `Accessibility - input fields must have an accessible label associated with it: ${applicableComponents.join(
", "
)}`
},
@@ -43,6 +44,7 @@ const rule = ESLintUtils.RuleCreator.withoutDocs({
// wrapped in Label tag, labelled with htmlFor, labelled with aria-labelledby
if (
+ hasNonEmptyProp(node.attributes, "aria-label") ||
hasFieldParent(context) ||
isInsideLabelTag(context) ||
hasAssociatedLabelViaHtmlFor(node, context) ||
diff --git a/lib/rules/tablist-and-tabs-need-labelling.js b/lib/rules/tablist-and-tabs-need-labelling.js
deleted file mode 100644
index aef89ba..0000000
--- a/lib/rules/tablist-and-tabs-need-labelling.js
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-"use strict";
-
-//------------------------------------------------------------------------------
-// Rule Definition
-//------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
- meta: {
- type: 'problem',
- docs: {
- description: 'This rule aims to ensure that Tabs with icons but no text labels have an accessible name and that Tablist is properly labeled.',
- recommended: true,
- url: 'https://www.w3.org/WAI/ARIA/apg/patterns/tabs/', // URL to the documentation page for this rule
- },
- fixable: null,
- schema: [],
- messages: {
- missingTabLabel: 'Accessibility: Tab elements must have an aria-label attribute is there is no visiable text content',
- missingTablistLabel: 'Accessibility: Tablist must have an accessible label'
- },
- },
-
- create(context) {
- const { hasTextContentChild } = require('../util/hasTextContentChild');
- const { hasNonEmptyProp } = require('../util/hasNonEmptyProp');
- const { hasAssociatedLabelViaAriaLabelledBy } = require('../util/labelUtils');
-
- var elementType = require("jsx-ast-utils").elementType;
-
- return {
-
- // visitor functions for different types of nodes
- JSXOpeningElement(node) {
- const elementTypeValue = elementType(node);
-
- // if it is not a Tablist or Tab, return
- if (elementTypeValue !== 'Tablist' && elementTypeValue !== 'Tab') {
- return;
- }
-
- // Check for Tablist elements
- if (elementTypeValue === "Tablist") {
- if (
- // if the Tablist has a label, if the Tablist has an associated label, return
- hasNonEmptyProp(node.attributes, 'aria-label') || //aria-label
- hasAssociatedLabelViaAriaLabelledBy(node, context) // aria-labelledby
- ) {
- return;
- }
- context.report({
- node,
- messageId: 'missingTablistLabel'
- });
- }
-
- // Check for Tab elements
- if (elementTypeValue === 'Tab') {
- if (
- hasTextContentChild(node.parent) || // text content
- hasNonEmptyProp(node.attributes, 'aria-label') // aria-label
- ) {
- return;
- }
- context.report({
- node,
- messageId: 'missingTabLabel'
- });
- }
- }
- };
- },
-};
diff --git a/lib/rules/tablist-and-tabs-need-labelling.ts b/lib/rules/tablist-and-tabs-need-labelling.ts
new file mode 100644
index 0000000..3545fc0
--- /dev/null
+++ b/lib/rules/tablist-and-tabs-need-labelling.ts
@@ -0,0 +1,77 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
+import { hasTextContentChild } from "../util/hasTextContentChild";
+import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
+import { hasAssociatedLabelViaAriaLabelledBy } from "../util/labelUtils";
+import { elementType } from "jsx-ast-utils";
+import { JSXOpeningElement } from "estree-jsx";
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+const rule = ESLintUtils.RuleCreator.withoutDocs({
+ defaultOptions: [],
+ meta: {
+ type: "problem",
+ docs: {
+ description:
+ "This rule aims to ensure that Tabs with icons but no text labels have an accessible name and that Tablist is properly labeled.",
+ recommended: "strict",
+ url: "https://www.w3.org/WAI/ARIA/apg/patterns/tabs/" // URL to the documentation page for this rule
+ },
+ fixable: undefined,
+ schema: [],
+ messages: {
+ missingTabLabel: "Accessibility: Tab elements must have an aria-label attribute is there is no visiable text content",
+ missingTablistLabel: "Accessibility: Tablist must have an accessible label"
+ }
+ },
+
+ create(context) {
+ return {
+ // visitor functions for different types of nodes
+ JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
+ const elementTypeValue = elementType(node as unknown as JSXOpeningElement);
+
+ // if it is not a Tablist or Tab, return
+ if (elementTypeValue !== "Tablist" && elementTypeValue !== "Tab") {
+ return;
+ }
+
+ // Check for Tablist elements
+ if (elementTypeValue === "Tablist") {
+ if (
+ // if the Tablist has a label, if the Tablist has an associated label, return
+ hasNonEmptyProp(node.attributes, "aria-label") || //aria-label
+ hasAssociatedLabelViaAriaLabelledBy(node, context) // aria-labelledby
+ ) {
+ return;
+ }
+ context.report({
+ node,
+ messageId: "missingTablistLabel"
+ });
+ }
+
+ // Check for Tab elements
+ if (elementTypeValue === "Tab") {
+ if (
+ hasTextContentChild(node.parent as unknown as TSESTree.JSXElement) || // text content
+ hasNonEmptyProp(node.attributes, "aria-label") // aria-label
+ ) {
+ return;
+ }
+ context.report({
+ node,
+ messageId: "missingTabLabel"
+ });
+ }
+ }
+ };
+ }
+});
+
+export default rule;
diff --git a/lib/rules/visual-label-better-than-aria-suggestion.js b/lib/rules/visual-label-better-than-aria-suggestion.ts
similarity index 65%
rename from lib/rules/visual-label-better-than-aria-suggestion.js
rename to lib/rules/visual-label-better-than-aria-suggestion.ts
index 1d167e7..c2226ec 100644
--- a/lib/rules/visual-label-better-than-aria-suggestion.js
+++ b/lib/rules/visual-label-better-than-aria-suggestion.ts
@@ -1,18 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-"use strict";
-
-var elementType = require("jsx-ast-utils").elementType;
-const { hasNonEmptyProp } = require("../util/hasNonEmptyProp");
-const { applicableComponents } = require("../applicableComponents/buttonBasedComponents");
+import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
+import { applicableComponents } from "../applicableComponents/inputBasedComponents";
+import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
+import { elementType } from "jsx-ast-utils";
+import { JSXOpeningElement } from "estree-jsx";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule = ESLintUtils.RuleCreator.withoutDocs({
+ defaultOptions: [],
meta: {
// possible warning messages for the lint rule
messages: {
@@ -20,20 +20,21 @@ module.exports = {
},
type: "suggestion", // `problem`, `suggestion`, or `layout`
docs: {
- description: "Visual label is better than an aria-label",
- recommended: true,
- url: null // URL to the documentation page for this rule
+ description: "Visual label is better than an aria-label because sighted users can't read the aria-label text.",
+ recommended: "strict",
+ url: undefined // URL to the documentation page for this rule
},
- fixable: null, // Or `code` or `whitespace`
+ fixable: undefined, // Or `code` or `whitespace`
schema: [] // Add a schema if the rule has options
},
+
// create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
create(context) {
return {
// visitor functions for different types of nodes
- JSXOpeningElement(node) {
+ JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
// if it is not a listed component, return
- if (!applicableComponents.includes(elementType(node))) {
+ if (!applicableComponents.includes(elementType(node as unknown as JSXOpeningElement))) {
return;
}
@@ -47,4 +48,6 @@ module.exports = {
}
};
}
-};
+});
+
+export default rule;
diff --git a/lib/util/flattenChildren.js b/lib/util/flattenChildren.js
deleted file mode 100644
index d8ff7f3..0000000
--- a/lib/util/flattenChildren.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-// TODO: add comments
-function flattenChildren(node) {
- const flatChildren = [];
-
- if (node.children && node.children.length > 0) {
- node.children.forEach(child => {
- if (child.type === 'JSXElement' && child.children && child.children.length > 0) {
- flatChildren.push(child, ...flattenChildren(child));
- } else {
- flatChildren.push(child);
- }
- });
- }
-
- return flatChildren;
-}
-
-module.exports.flattenChildren = flattenChildren;
diff --git a/lib/util/flattenChildren.ts b/lib/util/flattenChildren.ts
new file mode 100644
index 0000000..c8b285a
--- /dev/null
+++ b/lib/util/flattenChildren.ts
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { TSESTree } from "@typescript-eslint/types";
+
+// Flatten the JSX tree structure by recursively collecting all child elements
+const flattenChildren = (node: TSESTree.JSXElement): TSESTree.JSXElement[] => {
+ const flatChildren: TSESTree.JSXElement[] = [];
+
+ if (node.children && node.children.length > 0) {
+ node.children.forEach(child => {
+ if (child.type === "JSXElement") {
+ const jsxChild = child as TSESTree.JSXElement;
+ flatChildren.push(jsxChild, ...flattenChildren(jsxChild));
+ }
+ });
+ }
+
+ return flatChildren;
+};
+
+export { flattenChildren };
diff --git a/lib/util/hasFieldParent.js b/lib/util/hasFieldParent.js
deleted file mode 100644
index 223d1e4..0000000
--- a/lib/util/hasFieldParent.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-var elementType = require("jsx-ast-utils").elementType;
-
-function hasFieldParent(context) {
- const ancestors = context.getAncestors();
-
- if (ancestors == null || ancestors.length === 0) {
- return false;
- }
-
- var field = false;
-
- ancestors.forEach(item => {
- if (
- item.type === "JSXElement" &&
- item.openingElement != null &&
- item.openingElement.type === "JSXOpeningElement" &&
- elementType(item.openingElement) === "Field"
- ) {
- field = true;
- }
- });
-
- return field;
-}
-
-module.exports = {
- hasFieldParent
-};
diff --git a/lib/util/hasFieldParent.ts b/lib/util/hasFieldParent.ts
new file mode 100644
index 0000000..dd4605c
--- /dev/null
+++ b/lib/util/hasFieldParent.ts
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { TSESTree } from "@typescript-eslint/types";
+import { elementType } from "jsx-ast-utils";
+import { TSESLint } from "@typescript-eslint/utils";
+import { JSXOpeningElement } from "estree-jsx";
+
+// Function to check if the current node has a "Field" parent JSXElement
+export const hasFieldParent = (context: TSESLint.RuleContext): boolean => {
+ const ancestors: TSESTree.Node[] = context.getAncestors();
+
+ if (ancestors == null || ancestors.length === 0) {
+ return false;
+ }
+
+ let field = false;
+
+ ancestors.forEach(item => {
+ if (
+ item.type === "JSXElement" &&
+ item.openingElement != null &&
+ item.openingElement.type === "JSXOpeningElement" &&
+ elementType(item.openingElement as unknown as JSXOpeningElement) === "Field"
+ ) {
+ field = true;
+ }
+ });
+
+ return field;
+};
diff --git a/lib/util/hasLabelledChildImage.js b/lib/util/hasLabelledChildImage.js
deleted file mode 100644
index 9b5684d..0000000
--- a/lib/util/hasLabelledChildImage.js
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-const { flattenChildren } = require("./flattenChildren");
-const { hasProp, getPropValue } = require("jsx-ast-utils");
-const { hasNonEmptyProp } = require("./hasNonEmptyProp");
-const { fluentImageComponents, imageDomNodes } = require("../applicableComponents/imageBasedComponents");
-
-const mergedImageComponents = [...fluentImageComponents, ...imageDomNodes];
-
-/**
- * hasLabelledChildImage - determines if a component has text content as a child e.g.
- * @param {*} node JSXElement
- * @returns boolean
- */
-function hasLabelledChildImage(node) {
- // no children
- if (node.children == null || node.children == undefined || node.children.length === 0) {
- return false;
- }
-
- // Check if there is an accessible image
- const hasAccessibleImage = flattenChildren(node).some(child => {
- if (child.type === "JSXElement" && mergedImageComponents.includes(child.openingElement.name.name)) {
- return hasProp(child.openingElement.attributes, "aria-hidden") || getPropValue(child.openingElement.attributes, "alt")
- ? false
- : hasNonEmptyProp(child.openingElement.attributes, "title") ||
- hasNonEmptyProp(child.openingElement.attributes, "alt") ||
- hasNonEmptyProp(child.openingElement.attributes, "aria-label") ||
- hasNonEmptyProp(child.openingElement.attributes, "aria-labelledby");
- }
- return false;
- });
-
- return hasAccessibleImage;
-}
-
-module.exports = {
- hasLabelledChildImage
-};
diff --git a/lib/util/hasLabelledChildImage.ts b/lib/util/hasLabelledChildImage.ts
new file mode 100644
index 0000000..443941c
--- /dev/null
+++ b/lib/util/hasLabelledChildImage.ts
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { flattenChildren } from "./flattenChildren";
+import { TSESTree } from "@typescript-eslint/types";
+import { hasProp, getPropValue, getProp } from "jsx-ast-utils";
+import { hasNonEmptyProp } from "./hasNonEmptyProp";
+import { fluentImageComponents, imageDomNodes } from "../applicableComponents/imageBasedComponents";
+import { JSXOpeningElement } from "estree-jsx";
+
+const mergedImageComponents = [...fluentImageComponents, ...imageDomNodes];
+
+/**
+ * Checks if a JSX element name is a JSXIdentifier and matches a component name.
+ * @param name JSXTagNameExpression
+ * @returns boolean
+ */
+const isJSXIdentifierWithName = (name: TSESTree.JSXTagNameExpression, validNames: string[]): boolean => {
+ return name.type === "JSXIdentifier" && validNames.includes(name.name);
+};
+
+/**
+ * Determines if a component has an accessible image as a child.
+ * @param {*} node JSXElement
+ * @returns boolean
+ */
+const hasLabelledChildImage = (node: TSESTree.JSXElement): boolean => {
+ console.log("node::", node);
+ if (!node.children || node.children.length === 0) {
+ return false;
+ }
+
+ return flattenChildren(node).some(child => {
+ if (child.type === "JSXElement" && isJSXIdentifierWithName(child.openingElement.name, mergedImageComponents)) {
+ const attributes = child.openingElement.attributes;
+ console.log("attributes::", attributes);
+ console.log("hasAccessibilityAttributes(attributes)", hasAccessibilityAttributes(attributes));
+ console.log("!isImageHidden(attributes)", !isImageHidden(attributes));
+
+ return !isImageHidden(attributes) && hasAccessibilityAttributes(attributes);
+ }
+ return false;
+ });
+};
+
+/**
+ * Checks if an image element has any of the attributes indicating it is accessible.
+ * @param {*} attributes JSX attributes of the image element
+ * @returns boolean
+ */
+const hasAccessibilityAttributes = (attributes: TSESTree.JSXOpeningElement["attributes"]): boolean => {
+ return (
+ hasNonEmptyProp(attributes, "title") ||
+ hasNonEmptyProp(attributes, "alt") ||
+ hasNonEmptyProp(attributes, "aria-label") ||
+ hasNonEmptyProp(attributes, "aria-labelledby")
+ );
+};
+
+/**
+ * Checks if an image element is marked as hidden using `aria-hidden` or has an empty `alt` attribute.
+ * @param {*} attributes JSX attributes of the image element
+ * @returns boolean
+ */
+const isImageHidden = (attributes: TSESTree.JSXOpeningElement["attributes"]): boolean => {
+ // Check if the image has the `aria-hidden` attribute
+ if (hasProp(attributes as unknown as JSXOpeningElement["attributes"], "aria-hidden")) {
+ return true;
+ }
+
+ // Check if the image has an `aria-label` attribute with a non-empty value
+ const ariaLabelProp = getProp(attributes as unknown as JSXOpeningElement["attributes"], "aria-label");
+ if (ariaLabelProp) {
+ const ariaLabelValue = getPropValue(ariaLabelProp);
+ if (ariaLabelValue) {
+ return false; // If `aria-label` is present and has a value, the image is not hidden
+ }
+ }
+
+ // Check if the image has an `alt` attribute and return true if the `alt` value is falsy
+ const altProp = getProp(attributes as unknown as JSXOpeningElement["attributes"], "alt");
+ if (altProp) {
+ const altValue = getPropValue(altProp);
+ return !altValue; // Returns true if `altValue` is falsy (e.g., empty string, null, or undefined)
+ }
+
+ return true; // If neither `alt` nor `aria-label` is present, consider the image hidden
+};
+
+export { hasLabelledChildImage, isImageHidden, hasAccessibilityAttributes, isJSXIdentifierWithName };
diff --git a/lib/util/hasNonEmptyProp.js b/lib/util/hasNonEmptyProp.js
deleted file mode 100644
index 2d35771..0000000
--- a/lib/util/hasNonEmptyProp.js
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-var hasProp = require("jsx-ast-utils").hasProp;
-var getPropValue = require("jsx-ast-utils").getPropValue;
-var getProp = require("jsx-ast-utils").getProp;
-
-/**
- * Determines if the prop exists and has a non-empty value.
- * @param {*} attributes
- * @param {*} name
- * @returns boolean
- */
-function hasNonEmptyProp(attributes, name) {
- if (!hasProp(attributes, name)) {
- return false;
- }
-
- const propValue = getPropValue(getProp(attributes, name));
-
- /**
- * getPropValue internally normalizes "true", "false" to boolean values.
- * So it is sufficent to check if the prop exists and return.
- */
- if (typeof propValue === "boolean" || typeof propValue === "number") {
- return true;
- }
-
- return propValue.trim().length > 0;
-}
-
-module.exports.hasNonEmptyProp = hasNonEmptyProp;
-
diff --git a/lib/util/hasNonEmptyProp.ts b/lib/util/hasNonEmptyProp.ts
new file mode 100644
index 0000000..83a2fb7
--- /dev/null
+++ b/lib/util/hasNonEmptyProp.ts
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { TSESTree } from "@typescript-eslint/utils";
+import { JSXOpeningElement } from "estree-jsx";
+import { hasProp, getPropValue, getProp } from "jsx-ast-utils";
+
+/**
+ * Determines if the prop exists and has a non-empty value.
+ * @param {*} attributes
+ * @param {*} name
+ * @returns boolean
+ */
+const hasNonEmptyProp = (attributes: TSESTree.JSXOpeningElement["attributes"], name: string): boolean => {
+ if (!hasProp(attributes as unknown as JSXOpeningElement["attributes"], name)) {
+ return false;
+ }
+
+ const prop = getProp(attributes as unknown as JSXOpeningElement["attributes"], name);
+
+ // Safely get the value of the prop, handling potential undefined or null values
+ const propValue = prop ? getPropValue(prop) : undefined;
+
+ // Check for various types that `getPropValue` could return
+ if (propValue === null || propValue === undefined) {
+ return false;
+ }
+
+ if (typeof propValue === "boolean" || typeof propValue === "number") {
+ // Booleans and numbers are considered non-empty if they exist
+ return true;
+ }
+
+ if (typeof propValue === "string") {
+ // For strings, check if it is non-empty
+ return propValue.trim().length > 0;
+ }
+
+ // Handle other potential types (e.g., arrays, objects)
+ if (Array.isArray(propValue)) {
+ return propValue.length > 0;
+ }
+
+ if (typeof propValue === "object") {
+ // Objects are considered non-empty if they have properties
+ return Object.keys(propValue).length > 0;
+ }
+
+ // If the type is not handled, return false as a fallback
+ return false;
+};
+
+export { hasNonEmptyProp };
diff --git a/lib/util/hasTextContentChild.js b/lib/util/hasTextContentChild.ts
similarity index 74%
rename from lib/util/hasTextContentChild.js
rename to lib/util/hasTextContentChild.ts
index 70299ed..5f68401 100644
--- a/lib/util/hasTextContentChild.js
+++ b/lib/util/hasTextContentChild.ts
@@ -1,13 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+import { TSESTree } from "@typescript-eslint/types";
+
/**
* hasTextContentChild - determines if a component has text content as a child e.g.
* @param {*} node JSXElement
* @returns boolean
*/
-function hasTextContentChild(node) {
+const hasTextContentChild = (node?: TSESTree.JSXElement) => {
// no children
+ if (!node) {
+ return false;
+ }
+
if (node.children == null || node.children == undefined || node.children.length === 0) {
return false;
}
@@ -17,8 +23,6 @@ function hasTextContentChild(node) {
});
return result.length !== 0;
-}
-
-module.exports = {
- hasTextContentChild
};
+
+export { hasTextContentChild };
diff --git a/lib/util/labelUtils.js b/lib/util/labelUtils.js
deleted file mode 100644
index f3179b6..0000000
--- a/lib/util/labelUtils.js
+++ /dev/null
@@ -1,153 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-var elementType = require("jsx-ast-utils").elementType;
-var getPropValue = require("jsx-ast-utils").getPropValue;
-var getProp = require("jsx-ast-utils").getProp;
-const { hasNonEmptyProp } = require("./hasNonEmptyProp");
-
-/**
- * Checks if the element is nested within a Label tag.
- * e.g.
- *
- * @param {*} context
- * @returns
- */
-function isInsideLabelTag(context) {
- return context
- .getAncestors()
- .some(
- node =>
- node.type === "JSXElement" && (elementType(node.openingElement) === "Label" || elementType(node.openingElement) === "label")
- );
-}
-
-/**
- * Checks if there is a Label component inside the source code with a htmlFor attribute matching that of the id parameter.
- * e.g.
- * id=parameter,
- * @param {*} idValue
- * @param {*} context
- * @returns boolean for match found or not.
- */
-function hasLabelWithHtmlForId(idValue, context) {
- if (idValue === "") {
- return false;
- }
- const sourceCode = context.getSourceCode();
- const regex = /
+ * Sample input label
+ *
+ * @param {*} openingElement
+ * @param {*} context
+ * @param {*} ariaAttribute
+ * @returns boolean for match found or not.
+ */
+const hasAssociatedAriaText = (
+ openingElement: TSESTree.JSXOpeningElement,
+ context: TSESLint.RuleContext,
+ ariaAttribute: string
+) => {
+ const hasAssociatedAriaText = hasNonEmptyProp(openingElement.attributes, ariaAttribute);
+
+ const prop = getProp(openingElement.attributes as unknown as JSXOpeningElement["attributes"], ariaAttribute);
+
+ const idValue = prop ? getPropValue(prop) : undefined;
+
+ let hasHtmlId = false;
+ if (idValue) {
+ const sourceCode = context.getSourceCode();
+
+ const regex = /<(\w+)[^>]*id\s*=\s*["']([^"']*)["'][^>]*>/gi;
+ let match;
+ const ids = [];
+
+ while ((match = regex.exec(sourceCode.text)) !== null) {
+ ids.push(match[2]);
+ }
+ hasHtmlId = ids.some(id => id === idValue);
+ }
+
+ return hasAssociatedAriaText && hasHtmlId;
+};
+
+export {
+ isInsideLabelTag,
+ hasLabelWithHtmlForId,
+ hasLabelWithHtmlId,
+ hasAssociatedLabelViaAriaLabelledBy,
+ hasAssociatedLabelViaHtmlFor,
+ hasAssociatedLabelViaAriaDescribedby,
+ hasAssociatedAriaText,
+ hasOtherElementWithHtmlId
+};
diff --git a/tests/lib/rules/field-needs-labelling.js b/tests/lib/rules/field-needs-labelling.test.ts
similarity index 54%
rename from tests/lib/rules/field-needs-labelling.js
rename to tests/lib/rules/field-needs-labelling.test.ts
index 56fb68e..c407efa 100644
--- a/tests/lib/rules/field-needs-labelling.js
+++ b/tests/lib/rules/field-needs-labelling.test.ts
@@ -6,22 +6,17 @@
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
-
-const rule = require("../../../lib/rules/field-needs-labelling"),
- RuleTester = require("eslint").RuleTester;
+import { Rule } from "eslint";
+import ruleTester from "./helper/ruleTester";
+import rule from "../../../lib/rules/field-needs-labelling";
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
-const ruleTester = new RuleTester();
-ruleTester.run("field-needs-labelling", rule, {
+ruleTester.run("field-needs-labelling", rule as unknown as Rule.RuleModule, {
valid: [
- `
+ `
`,
`
-
- `,
+ code: ``,
errors: [{ messageId: "noUnlabelledField" }]
},
{
- code: `
-
- `,
+ code: ``,
errors: [{ messageId: "noUnlabelledField" }]
}
]
});
-
diff --git a/tests/lib/rules/input-components-require-accessible-name.test.ts b/tests/lib/rules/input-components-require-accessible-name.test.ts
index f29bacb..e1baf33 100644
--- a/tests/lib/rules/input-components-require-accessible-name.test.ts
+++ b/tests/lib/rules/input-components-require-accessible-name.test.ts
@@ -9,19 +9,36 @@ import { Rule } from "eslint";
import ruleTester from "./helper/ruleTester";
import rule from "../../../lib/rules/input-components-require-accessible-name";
import { applicableComponents } from "../../../lib/applicableComponents/inputBasedComponents";
+import { labelBasedComponents, elementsUsedAsLabels } from "../../../lib/applicableComponents/labelBasedComponents";
//------------------------------------------------------------------------------
// Helper function to generate test cases
//------------------------------------------------------------------------------
-function generateTestCases(componentName: string) {
+function generateTestCases(labelComponent: string, componentName: string) {
return {
valid: [
- `<>Some Label<${componentName} id="some-id"/>>`,
- `<>Some Label<${componentName} id="some-id" aria-labelledby="test-span"/>>`,
- `test`,
- `test<${componentName} />`,
- `test<${componentName} />`,
- `<${componentName} />`
+ `<><${labelComponent} id="test-span">Some Label${labelComponent}><${componentName} id="some-id" aria-labelledby="test-span"/>>`
+ ],
+ invalid: [
+ {
+ code: `<><${labelComponent} id="test-span-2">Some Label${labelComponent}><${componentName} id="some-id" aria-labelledby="test-span"/>>`,
+ errors: [{ messageId: "missingLabelOnInput" }]
+ }
+ ]
+ };
+}
+
+function generateTestCasesLabel(labelComponent: string, componentName: string) {
+ return {
+ valid: [
+ `<><${labelComponent} htmlFor="some-id">Some Label${labelComponent}><${componentName} id="some-id"/>>`,
+ `<><${labelComponent} id="test-span">Some Label${labelComponent}><${componentName} id="some-id" aria-labelledby="test-span"/>>`,
+ `<${labelComponent}>test${labelComponent}>`,
+ `<${labelComponent}>test<${componentName} />${labelComponent}>`,
+ `<${labelComponent}>test<${componentName} />${labelComponent}>`,
+ `<${componentName} />`,
+ `<${componentName} aria-label="this is my component" />`,
+ `<><${labelComponent} id="paragraph_label-2">type here${labelComponent}><${componentName} aria-labelledby="paragraph_label-2">${componentName}><${labelComponent} id="paragraph_label-3">type here${labelComponent}><${componentName} aria-labelledby="paragraph_label-3">${componentName}>>`
],
invalid: [
{
@@ -29,11 +46,11 @@ function generateTestCases(componentName: string) {
errors: [{ messageId: "missingLabelOnInput" }]
},
{
- code: `<><${componentName}/>>`,
+ code: `<><${labelComponent}/><${componentName}/>>`,
errors: [{ messageId: "missingLabelOnInput" }]
},
{
- code: `<><${componentName} />>`,
+ code: `<><${labelComponent} htmlFor="id"/><${componentName} />>`,
errors: [{ messageId: "missingLabelOnInput" }]
},
{
@@ -41,7 +58,7 @@ function generateTestCases(componentName: string) {
errors: [{ messageId: "missingLabelOnInput" }]
},
{
- code: `<>Some Label<${componentName} id="some-id"/>>`,
+ code: `<><${labelComponent}>Some Label${labelComponent}><${componentName} id="some-id"/>>`,
errors: [{ messageId: "missingLabelOnInput" }]
},
{
@@ -52,14 +69,34 @@ function generateTestCases(componentName: string) {
};
}
+function generateAllTestCases() {
+ const testSets: any[] = [];
+
+ // For each input-based component, generate test cases
+
+ applicableComponents.forEach(components => {
+ elementsUsedAsLabels.forEach(labels => {
+ testSets.push(generateTestCases(labels, components));
+ });
+
+ // Also generate test cases for each native DOM element
+ labelBasedComponents.forEach(labels => {
+ testSets.push(generateTestCasesLabel(labels, components));
+ });
+ });
+
+ return testSets;
+}
+
// Collect all test cases for all applicable components
-const allTestCases = applicableComponents.flatMap(component => generateTestCases(component));
+const allTestCases = generateAllTestCases();
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
-
-ruleTester.run("input-missing-label", rule as unknown as Rule.RuleModule, {
- valid: allTestCases.flatMap(test => test.valid),
- invalid: allTestCases.flatMap(test => test.invalid)
+allTestCases.forEach((testCaseSet, index) => {
+ ruleTester.run(`input-missing-label test set ${index + 1}`, rule as unknown as Rule.RuleModule, {
+ valid: testCaseSet.valid,
+ invalid: testCaseSet.invalid
+ });
});
diff --git a/tests/lib/rules/label-utils.test.ts b/tests/lib/rules/label-utils.test.ts
deleted file mode 100644
index 8c2017b..0000000
--- a/tests/lib/rules/label-utils.test.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-import chai from "chai";
-import { isInsideLabelTag } from "../../../lib/util/labelUtils";
-
-const assert: Chai.AssertStatic = chai.assert;
-
-console.log(assert);
-describe("isInsideLabelTag", function () {
- it("should return true when nested within a Label tag", function () {
- const context = {
- getAncestors: () => [
- {
- type: "JSXElement",
- openingElement: { name: { name: "Label" } }
- }
- // Other ancestors as needed...
- ]
- };
-
- const result = isInsideLabelTag(context);
-
- assert.isTrue(result);
- });
-
- it("should return true when nested within a label tag (case-insensitive)", function () {
- const context = {
- getAncestors: () => [
- {
- type: "JSXElement",
- openingElement: { name: { name: "label" } }
- }
- // Other ancestors as needed...
- ]
- };
-
- const result = isInsideLabelTag(context);
-
- assert.isTrue(result);
- });
-
- it("should return false when not nested within a Label tag", function () {
- const context = {
- getAncestors: () => [
- {
- type: "JSXElement",
- openingElement: { name: { name: "div" } } // Non-Label element
- }
- // Other ancestors as needed...
- ]
- };
-
- const result = isInsideLabelTag(context);
-
- assert.isFalse(result);
- });
-});
diff --git a/tests/lib/rules/link-missing-labelling.test.ts b/tests/lib/rules/link-missing-labelling.test.ts
index f094522..1655dfc 100644
--- a/tests/lib/rules/link-missing-labelling.test.ts
+++ b/tests/lib/rules/link-missing-labelling.test.ts
@@ -24,6 +24,7 @@ function generateTestCases(componentName: string, imageName: string) {
`<>This is a Header<${componentName} href="https://www.bing.com" aria-labelledby="my-label-2"><${imageName} src="img_girl.jpg" />${componentName}>>`,
`<${componentName} href="https://www.bing.com"><${imageName} src="img1.jpg" /><${imageName} src="img2.jpg" alt="The girl with the dog." />${componentName}>`
],
+
invalid: [
// Invalid cases
{
diff --git a/tests/lib/rules/tablist-and-tabs-need-labelling.js b/tests/lib/rules/tablist-and-tabs-need-labelling.test.ts
similarity index 65%
rename from tests/lib/rules/tablist-and-tabs-need-labelling.js
rename to tests/lib/rules/tablist-and-tabs-need-labelling.test.ts
index ecb8ed0..8e72538 100644
--- a/tests/lib/rules/tablist-and-tabs-need-labelling.js
+++ b/tests/lib/rules/tablist-and-tabs-need-labelling.test.ts
@@ -1,45 +1,33 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-"use strict";
+import { Rule } from "eslint";
+import ruleTester from "./helper/ruleTester";
+import rule from "../../../lib/rules/tablist-and-tabs-need-labelling";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
-const rule = require("../../../lib/rules/tablist-and-tabs-need-labelling"),
- RuleTester = require("eslint").RuleTester;
-
-RuleTester.setDefaultConfig({
- parserOptions: {
- ecmaVersion: 6,
- ecmaFeatures: {
- jsx: true
- }
- }
-});
-
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
-
-const ruleTester = new RuleTester();
-ruleTester.run("tablist-and-tabs-need-labelling", rule, {
+ruleTester.run("tablist-and-tabs-need-labelling", rule as unknown as Rule.RuleModule, {
valid: [
// Valid cases for Tablist
'Settings Tab',
'Settings LabelSettings Tab',
-
+
// Valid cases
'} aria-label="Settings" />',
- '}>Settings',
- 'Settings',
+ "}>Settings",
+ "Settings"
],
invalid: [
// Invalid cases for Tablist
{
- code: 'Settings Tab',
+ code: "Settings Tab",
errors: [{ messageId: "missingTablistLabel" }]
},
{
@@ -47,18 +35,18 @@ ruleTester.run("tablist-and-tabs-need-labelling", rule, {
errors: [{ messageId: "missingTablistLabel" }]
},
{
- code: 'Settings LabelSettings Tab',
+ code: "Settings LabelSettings Tab",
errors: [{ messageId: "missingTablistLabel" }]
},
-
+
// Invalid cases for Tab
{
- code: '} />',
+ code: "} />",
errors: [{ messageId: "missingTabLabel" }]
},
{
- code: '}>',
+ code: "}>",
errors: [{ messageId: "missingTabLabel" }]
- },
+ }
]
});
diff --git a/tests/lib/rules/utils/flattenChildren.test.ts b/tests/lib/rules/utils/flattenChildren.test.ts
new file mode 100644
index 0000000..6a42b3d
--- /dev/null
+++ b/tests/lib/rules/utils/flattenChildren.test.ts
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { flattenChildren } from "../../../../lib/util/flattenChildren";
+import { TSESTree } from "@typescript-eslint/types";
+
+describe("flattenChildren", () => {
+ it("should return an empty array when there are no children", () => {
+ const node: TSESTree.JSXElement = {
+ children: []
+ } as any;
+ expect(flattenChildren(node)).toEqual([]);
+ });
+
+ it("should return direct children when there are no nested children", () => {
+ const child1: TSESTree.JSXElement = { children: [], type: "JSXElement" } as any;
+ const child2: TSESTree.JSXElement = { children: [], type: "JSXElement" } as any;
+ const node: TSESTree.JSXElement = {
+ children: [child1, child2]
+ } as any;
+
+ expect(flattenChildren(node)).toEqual([child1, child2]);
+ });
+
+ it("should return a flattened array of children with nested JSXElements", () => {
+ const nestedChild: TSESTree.JSXElement = { children: [], type: "JSXElement" } as any;
+ const child: TSESTree.JSXElement = { children: [nestedChild], type: "JSXElement" } as any;
+ const root: TSESTree.JSXElement = { children: [child], type: "JSXElement" } as any;
+
+ expect(flattenChildren(root)).toEqual([child, nestedChild]);
+ });
+
+ it("should ignore non-JSXElement children", () => {
+ const child: TSESTree.JSXElement = { children: [], type: "JSXElement" } as any;
+ const nonJSXChild = { type: "JSXText", value: "Hello" } as any;
+ const root: TSESTree.JSXElement = { children: [child, nonJSXChild], type: "JSXElement" } as any;
+
+ expect(flattenChildren(root)).toEqual([child]);
+ });
+
+ it("should handle complex nesting of JSXElements", () => {
+ const grandchild1: TSESTree.JSXElement = { children: [], type: "JSXElement" } as any;
+ const grandchild2: TSESTree.JSXElement = { children: [], type: "JSXElement" } as any;
+ const child1: TSESTree.JSXElement = { children: [grandchild1], type: "JSXElement" } as any;
+ const child2: TSESTree.JSXElement = { children: [grandchild2], type: "JSXElement" } as any;
+ const root: TSESTree.JSXElement = { children: [child1, child2], type: "JSXElement" } as any;
+
+ expect(flattenChildren(root)).toEqual([child1, grandchild1, child2, grandchild2]);
+ });
+});
diff --git a/tests/lib/rules/utils/hasFieldParent.test.ts b/tests/lib/rules/utils/hasFieldParent.test.ts
new file mode 100644
index 0000000..5e5d06c
--- /dev/null
+++ b/tests/lib/rules/utils/hasFieldParent.test.ts
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { hasFieldParent } from "../../../../lib/util/hasFieldParent";
+import { TSESTree } from "@typescript-eslint/types";
+import { TSESLint } from "@typescript-eslint/utils";
+import { elementType } from "jsx-ast-utils";
+
+// Mock the elementType function to return "Field" when needed
+jest.mock("jsx-ast-utils", () => ({
+ elementType: jest.fn()
+}));
+
+// Mock context to simulate ESLint's RuleContext
+const createMockContext = (ancestors: TSESTree.Node[]): TSESLint.RuleContext => ({
+ // Mock the required properties of RuleContext
+ id: "mockRule",
+ options: [],
+ settings: {},
+ parserPath: "",
+ parserOptions: {},
+ getCwd: jest.fn(),
+ getFilename: jest.fn(() => "mockFile.js"),
+ getScope: jest.fn(),
+ report: jest.fn(),
+ getAncestors: () => ancestors,
+ getSourceCode: jest.fn(),
+ getDeclaredVariables: function (node: TSESTree.Node): readonly TSESLint.Scope.Variable[] {
+ throw new Error("Function not implemented.");
+ },
+ markVariableAsUsed: function (name: string): boolean {
+ throw new Error("Function not implemented.");
+ }
+});
+
+describe("hasFieldParent", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("should return false when there are no ancestors", () => {
+ const mockContext = createMockContext([]);
+ const result = hasFieldParent(mockContext);
+ expect(result).toBe(false);
+ });
+
+ test("should return false when there are ancestors but none are JSXElements", () => {
+ const mockContext = createMockContext([{ type: "Literal" } as TSESTree.Literal]);
+ const result = hasFieldParent(mockContext);
+ expect(result).toBe(false);
+ });
+
+ test('should return false when none of the ancestors are "Field" elements', () => {
+ const mockContext = createMockContext([
+ {
+ type: "JSXElement",
+ openingElement: { type: "JSXOpeningElement" }
+ } as TSESTree.JSXElement
+ ]);
+ (elementType as jest.Mock).mockReturnValue("NotAField");
+ const result = hasFieldParent(mockContext);
+ expect(result).toBe(false);
+ });
+
+ test('should return true when one of the ancestors is a "Field" element', () => {
+ const mockContext = createMockContext([
+ {
+ type: "JSXElement",
+ openingElement: { type: "JSXOpeningElement" }
+ } as TSESTree.JSXElement
+ ]);
+ (elementType as jest.Mock).mockReturnValue("Field");
+ const result = hasFieldParent(mockContext);
+ expect(result).toBe(true);
+ });
+
+ test('should handle multiple ancestors with one "Field" element', () => {
+ const mockContext = createMockContext([
+ { type: "JSXElement", openingElement: { type: "JSXOpeningElement" } } as TSESTree.JSXElement,
+ { type: "Literal" } as TSESTree.Literal,
+ { type: "JSXElement", openingElement: { type: "JSXOpeningElement" } } as TSESTree.JSXElement
+ ]);
+ (elementType as jest.Mock).mockReturnValueOnce("NotAField").mockReturnValueOnce("Field");
+ const result = hasFieldParent(mockContext);
+ expect(result).toBe(true);
+ });
+});
diff --git a/tests/lib/rules/utils/hasLabelledChilImage.test.ts b/tests/lib/rules/utils/hasLabelledChilImage.test.ts
new file mode 100644
index 0000000..1c514e3
--- /dev/null
+++ b/tests/lib/rules/utils/hasLabelledChilImage.test.ts
@@ -0,0 +1,156 @@
+// Import necessary dependencies and mock functions
+import {
+ hasLabelledChildImage,
+ isImageHidden,
+ hasAccessibilityAttributes,
+ isJSXIdentifierWithName
+} from "../../../../lib/util/hasLabelledChildImage";
+import { TSESTree, AST_NODE_TYPES } from "@typescript-eslint/types";
+import { fluentImageComponents, imageDomNodes } from "../../../../lib/applicableComponents/imageBasedComponents";
+const mergedImageComponents = [...fluentImageComponents, ...imageDomNodes];
+
+// Helper function to create mock loc and range
+const createMockLocRange = () => ({
+ loc: { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } },
+ range: [0, 0] as [number, number]
+});
+
+// Unit tests
+describe("isJSXIdentifierWithName", () => {
+ it("returns true for a JSXIdentifier with a valid name", () => {
+ const name: TSESTree.JSXIdentifier = { type: AST_NODE_TYPES.JSXIdentifier, name: "img", ...createMockLocRange() };
+ expect(isJSXIdentifierWithName(name, mergedImageComponents)).toBe(true);
+ });
+
+ it("returns false for a JSXIdentifier with an invalid name", () => {
+ const name: TSESTree.JSXIdentifier = { type: AST_NODE_TYPES.JSXIdentifier, name: "div", ...createMockLocRange() };
+ expect(isJSXIdentifierWithName(name, mergedImageComponents)).toBe(false);
+ });
+});
+
+describe("hasAccessibilityAttributes", () => {
+ it("returns true if any accessible attribute is non-empty", () => {
+ const attributes: TSESTree.JSXOpeningElement["attributes"] = [
+ {
+ type: AST_NODE_TYPES.JSXAttribute,
+ name: { type: AST_NODE_TYPES.JSXIdentifier, name: "alt", ...createMockLocRange() },
+ value: {
+ type: AST_NODE_TYPES.Literal,
+ value: "An image description",
+ raw: '"An image description"',
+ ...createMockLocRange()
+ },
+ ...createMockLocRange()
+ }
+ ];
+ expect(hasAccessibilityAttributes(attributes)).toBe(true);
+ });
+
+ it("returns false if no accessible attribute is present", () => {
+ const attributes: TSESTree.JSXOpeningElement["attributes"] = [
+ {
+ type: AST_NODE_TYPES.JSXAttribute,
+ name: { type: AST_NODE_TYPES.JSXIdentifier, name: "aria-hidden", ...createMockLocRange() },
+ value: { type: AST_NODE_TYPES.Literal, value: "true", raw: '"true"', ...createMockLocRange() },
+ ...createMockLocRange()
+ }
+ ];
+ expect(hasAccessibilityAttributes(attributes)).toBe(false);
+ });
+});
+
+describe("isImageHidden", () => {
+ it("returns true if `aria-hidden` is set", () => {
+ const attributes: TSESTree.JSXOpeningElement["attributes"] = [
+ {
+ type: AST_NODE_TYPES.JSXAttribute,
+ name: { type: AST_NODE_TYPES.JSXIdentifier, name: "aria-hidden", ...createMockLocRange() },
+ value: { type: AST_NODE_TYPES.Literal, value: "true", raw: '"true"', ...createMockLocRange() },
+ ...createMockLocRange()
+ }
+ ];
+ expect(isImageHidden(attributes)).toBe(true);
+ });
+
+ it("returns true if `alt` attribute is empty", () => {
+ const attributes: TSESTree.JSXOpeningElement["attributes"] = [
+ {
+ type: AST_NODE_TYPES.JSXAttribute,
+ name: { type: AST_NODE_TYPES.JSXIdentifier, name: "alt", ...createMockLocRange() },
+ value: { type: AST_NODE_TYPES.Literal, value: "", raw: '""', ...createMockLocRange() },
+ ...createMockLocRange()
+ }
+ ];
+ expect(isImageHidden(attributes)).toBe(true);
+ });
+
+ it("returns false if `alt` attribute is non-empty", () => {
+ const attributes: TSESTree.JSXOpeningElement["attributes"] = [
+ {
+ type: AST_NODE_TYPES.JSXAttribute,
+ name: { type: AST_NODE_TYPES.JSXIdentifier, name: "alt", ...createMockLocRange() },
+ value: { type: AST_NODE_TYPES.Literal, value: "Image description", raw: '"Image description"', ...createMockLocRange() },
+ ...createMockLocRange()
+ }
+ ];
+ expect(isImageHidden(attributes)).toBe(false);
+ });
+});
+
+describe("hasLabelledChildImage", () => {
+ it("returns true if a child image component with accessibility attributes is found", () => {
+ const mockChild: TSESTree.JSXElement = {
+ type: AST_NODE_TYPES.JSXElement,
+ openingElement: {
+ type: AST_NODE_TYPES.JSXOpeningElement,
+ name: { type: AST_NODE_TYPES.JSXIdentifier, name: "img", ...createMockLocRange() },
+ attributes: [
+ {
+ type: AST_NODE_TYPES.JSXAttribute,
+ name: { type: AST_NODE_TYPES.JSXIdentifier, name: "alt", ...createMockLocRange() },
+ value: { type: AST_NODE_TYPES.Literal, value: "description", raw: '"description"', ...createMockLocRange() },
+ ...createMockLocRange()
+ }
+ ],
+ selfClosing: false,
+ ...createMockLocRange()
+ },
+ closingElement: null,
+ children: [],
+ ...createMockLocRange()
+ };
+
+ const node: TSESTree.JSXElement = {
+ type: AST_NODE_TYPES.JSXElement,
+ openingElement: {
+ type: AST_NODE_TYPES.JSXOpeningElement,
+ name: { type: AST_NODE_TYPES.JSXIdentifier, name: "Container", ...createMockLocRange() },
+ attributes: [],
+ selfClosing: false,
+ ...createMockLocRange()
+ },
+ closingElement: null,
+ children: [mockChild],
+ ...createMockLocRange()
+ };
+ expect(hasLabelledChildImage(node)).toBe(true);
+ });
+
+ it("returns false if no image component is found", () => {
+ const node: TSESTree.JSXElement = {
+ type: AST_NODE_TYPES.JSXElement,
+ openingElement: {
+ type: AST_NODE_TYPES.JSXOpeningElement,
+ name: { type: AST_NODE_TYPES.JSXIdentifier, name: "div", ...createMockLocRange() },
+ attributes: [],
+ selfClosing: false,
+ ...createMockLocRange()
+ },
+ closingElement: null,
+ children: [],
+ ...createMockLocRange()
+ };
+
+ expect(hasLabelledChildImage(node)).toBe(false);
+ });
+});
diff --git a/tests/lib/rules/utils/hasNonEmptyProp.test.ts b/tests/lib/rules/utils/hasNonEmptyProp.test.ts
new file mode 100644
index 0000000..b55a40e
--- /dev/null
+++ b/tests/lib/rules/utils/hasNonEmptyProp.test.ts
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { hasNonEmptyProp } from "../../../../lib/util/hasNonEmptyProp";
+import { TSESTree } from "@typescript-eslint/utils";
+import { getProp, getPropValue, hasProp } from "jsx-ast-utils";
+
+// Mocking getProp, getPropValue, and hasProp
+jest.mock("jsx-ast-utils", () => ({
+ hasProp: jest.fn(),
+ getProp: jest.fn(),
+ getPropValue: jest.fn()
+}));
+
+describe("hasNonEmptyProp", () => {
+ const attributes: TSESTree.JSXOpeningElement["attributes"] = [] as any;
+ const propName = "testProp";
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("should return false if the property does not exist", () => {
+ (hasProp as jest.Mock).mockReturnValue(false);
+ expect(hasNonEmptyProp(attributes, propName)).toBe(false);
+ });
+
+ it("should return false if the property value is undefined", () => {
+ (hasProp as jest.Mock).mockReturnValue(true);
+ (getProp as jest.Mock).mockReturnValue({});
+ (getPropValue as jest.Mock).mockReturnValue(undefined);
+ expect(hasNonEmptyProp(attributes, propName)).toBe(false);
+ });
+
+ it("should return false if the property value is null", () => {
+ (hasProp as jest.Mock).mockReturnValue(true);
+ (getProp as jest.Mock).mockReturnValue({});
+ (getPropValue as jest.Mock).mockReturnValue(null);
+ expect(hasNonEmptyProp(attributes, propName)).toBe(false);
+ });
+
+ it("should return true if the property value is a non-empty string", () => {
+ (hasProp as jest.Mock).mockReturnValue(true);
+ (getProp as jest.Mock).mockReturnValue({});
+ (getPropValue as jest.Mock).mockReturnValue("non-empty string");
+ expect(hasNonEmptyProp(attributes, propName)).toBe(true);
+ });
+
+ it("should return false if the property value is an empty string", () => {
+ (hasProp as jest.Mock).mockReturnValue(true);
+ (getProp as jest.Mock).mockReturnValue({});
+ (getPropValue as jest.Mock).mockReturnValue(" ");
+ expect(hasNonEmptyProp(attributes, propName)).toBe(false);
+ });
+
+ it("should return true if the property value is a non-zero number", () => {
+ (hasProp as jest.Mock).mockReturnValue(true);
+ (getProp as jest.Mock).mockReturnValue({});
+ (getPropValue as jest.Mock).mockReturnValue(42);
+ expect(hasNonEmptyProp(attributes, propName)).toBe(true);
+ });
+
+ it("should return true if the property value is a boolean", () => {
+ (hasProp as jest.Mock).mockReturnValue(true);
+ (getProp as jest.Mock).mockReturnValue({});
+ (getPropValue as jest.Mock).mockReturnValue(true);
+ expect(hasNonEmptyProp(attributes, propName)).toBe(true);
+ });
+
+ it("should return true if the property value is a non-empty array", () => {
+ (hasProp as jest.Mock).mockReturnValue(true);
+ (getProp as jest.Mock).mockReturnValue({});
+ (getPropValue as jest.Mock).mockReturnValue([1, 2, 3]);
+ expect(hasNonEmptyProp(attributes, propName)).toBe(true);
+ });
+
+ it("should return false if the property value is an empty array", () => {
+ (hasProp as jest.Mock).mockReturnValue(true);
+ (getProp as jest.Mock).mockReturnValue({});
+ (getPropValue as jest.Mock).mockReturnValue([]);
+ expect(hasNonEmptyProp(attributes, propName)).toBe(false);
+ });
+
+ it("should return true if the property value is a non-empty object", () => {
+ (hasProp as jest.Mock).mockReturnValue(true);
+ (getProp as jest.Mock).mockReturnValue({});
+ (getPropValue as jest.Mock).mockReturnValue({ key: "value" });
+ expect(hasNonEmptyProp(attributes, propName)).toBe(true);
+ });
+
+ it("should return false if the property value is an empty object", () => {
+ (hasProp as jest.Mock).mockReturnValue(true);
+ (getProp as jest.Mock).mockReturnValue({});
+ (getPropValue as jest.Mock).mockReturnValue({});
+ expect(hasNonEmptyProp(attributes, propName)).toBe(false);
+ });
+});
diff --git a/tests/lib/rules/utils/hasTextContentChild.test.ts b/tests/lib/rules/utils/hasTextContentChild.test.ts
new file mode 100644
index 0000000..056963e
--- /dev/null
+++ b/tests/lib/rules/utils/hasTextContentChild.test.ts
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { hasTextContentChild } from "../../../../lib/util/hasTextContentChild";
+import { TSESTree } from "@typescript-eslint/types";
+
+describe("hasTextContentChild", () => {
+ it("should return false when node is undefined", () => {
+ expect(hasTextContentChild(undefined)).toBe(false);
+ });
+
+ it("should return false when node.children is null or undefined", () => {
+ const node: TSESTree.JSXElement = { children: null } as any;
+ expect(hasTextContentChild(node)).toBe(false);
+
+ const nodeUndefinedChildren: TSESTree.JSXElement = { children: undefined } as any;
+ expect(hasTextContentChild(nodeUndefinedChildren)).toBe(false);
+ });
+
+ it("should return false when node.children is an empty array", () => {
+ const node: TSESTree.JSXElement = { children: [] } as any;
+ expect(hasTextContentChild(node)).toBe(false);
+ });
+
+ it("should return false when node.children has no JSXText elements with non-whitespace content", () => {
+ const node: TSESTree.JSXElement = {
+ children: [{ type: "JSXElement" }, { type: "JSXExpressionContainer" }]
+ } as any;
+ expect(hasTextContentChild(node)).toBe(false);
+ });
+
+ it("should return true when node.children has at least one JSXText element with non-whitespace content", () => {
+ const node: TSESTree.JSXElement = {
+ children: [{ type: "JSXText", value: "Hello" }, { type: "JSXElement" }]
+ } as any;
+ expect(hasTextContentChild(node)).toBe(true);
+ });
+
+ it("should return false when node.children has only whitespace in JSXText elements", () => {
+ const node: TSESTree.JSXElement = {
+ children: [{ type: "JSXText", value: " " }]
+ } as any;
+ expect(hasTextContentChild(node)).toBe(false);
+ });
+});
diff --git a/tests/lib/rules/utils/hasTooltipParent.test.ts b/tests/lib/rules/utils/hasTooltipParent.test.ts
index b048b33..5c356a5 100644
--- a/tests/lib/rules/utils/hasTooltipParent.test.ts
+++ b/tests/lib/rules/utils/hasTooltipParent.test.ts
@@ -1,3 +1,6 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
import { hasToolTipParent } from "../../../../lib/util/hasTooltipParent";
import { TSESLint } from "@typescript-eslint/utils";
diff --git a/tests/lib/rules/utils/labelUtils.test.ts b/tests/lib/rules/utils/labelUtils.test.ts
new file mode 100644
index 0000000..9d75962
--- /dev/null
+++ b/tests/lib/rules/utils/labelUtils.test.ts
@@ -0,0 +1,212 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { hasAssociatedLabelViaAriaLabelledBy, hasAssociatedLabelViaAriaDescribedby } from "../../../../lib/util/labelUtils";
+
+import { TSESTree, TSESLint, AST_NODE_TYPES } from "@typescript-eslint/utils"; // Use TSESTree types consistently
+
+describe("labelUtils", () => {
+ // Mock context with getSourceCode method
+ const mockContext = (): TSESLint.RuleContext => {
+ return {
+ getSourceCode: () => ({
+ getText: () => "mocked text"
+ })
+ } as unknown as TSESLint.RuleContext;
+ };
+ // Define the test suite
+ describe("hasAssociatedLabelViaAriaLabelledBy", () => {
+ let context: TSESLint.RuleContext;
+ let openingElement: TSESTree.JSXOpeningElement;
+
+ beforeEach(() => {
+ context = mockContext();
+ openingElement = {
+ attributes: []
+ } as unknown as TSESTree.JSXOpeningElement;
+ });
+
+ function createJSXAttribute(name: string, value: string | number | null): TSESTree.JSXAttribute {
+ return {
+ type: AST_NODE_TYPES.JSXAttribute,
+ name: { type: AST_NODE_TYPES.JSXIdentifier, name } as TSESTree.JSXIdentifier,
+ value: value !== null ? ({ type: AST_NODE_TYPES.Literal, value } as TSESTree.Literal) : null,
+ loc: {} as TSESTree.SourceLocation,
+ range: [0, 0]
+ };
+ }
+
+ test("returns false if aria-labelledby is missing", () => {
+ const result = hasAssociatedLabelViaAriaLabelledBy(openingElement, context);
+ expect(result).toBe(false);
+ });
+
+ test("returns false if aria-labelledby is empty", () => {
+ openingElement.attributes = [createJSXAttribute("aria-labelledby", "")];
+ const result = hasAssociatedLabelViaAriaLabelledBy(openingElement, context);
+ expect(result).toBe(false);
+ });
+
+ test("returns false if aria-labelledby value is not a string", () => {
+ openingElement.attributes = [createJSXAttribute("aria-labelledby", 123)];
+ const result = hasAssociatedLabelViaAriaLabelledBy(openingElement, context);
+ expect(result).toBe(false);
+ });
+
+ test("returns false if referenced element by id does not exist", () => {
+ const customContext: TSESLint.RuleContext = {
+ getSourceCode: () => ({
+ getText: () => "Test Label",
+ text: () => "Test Label"
+ })
+ } as unknown as TSESLint.RuleContext;
+
+ openingElement.attributes = [createJSXAttribute("aria-labelledby", "non-existing-id")];
+ const result = hasAssociatedLabelViaAriaLabelledBy(openingElement, customContext);
+ expect(result).toBe(false);
+ });
+
+ test("returns true if aria-labelledby references an existing label element", () => {
+ const customContext: TSESLint.RuleContext = {
+ getSourceCode: () => ({
+ getText: () => "Test Label",
+ text: () => "Test Label"
+ })
+ } as unknown as TSESLint.RuleContext;
+
+ openingElement.attributes = [createJSXAttribute("aria-labelledby", "existing-label-id")];
+ const result = hasAssociatedLabelViaAriaLabelledBy(openingElement, customContext);
+ expect(result).toBe(true);
+ });
+
+ test("returns true if aria-labelledby references an existing label element without duplicates", () => {
+ const customContext: TSESLint.RuleContext = {
+ getSourceCode: () => ({
+ getText: () => "Test LabelTest Label",
+ text: () => "Test Label"
+ })
+ } as unknown as TSESLint.RuleContext;
+
+ openingElement.attributes = [createJSXAttribute("aria-labelledby", "existing-label-id")];
+ const result = hasAssociatedLabelViaAriaLabelledBy(openingElement, customContext);
+ expect(result).toBe(true);
+ });
+
+ test("returns true if aria-labelledby references an existing non-label element", () => {
+ const customContext: TSESLint.RuleContext = {
+ getSourceCode: () => ({
+ getText: () => "Test Label
",
+ text: () => "Test Label
"
+ })
+ } as unknown as TSESLint.RuleContext;
+
+ openingElement.attributes = [createJSXAttribute("aria-labelledby", "existing-non-label-id")];
+ const result = hasAssociatedLabelViaAriaLabelledBy(openingElement, customContext);
+ expect(result).toBe(true);
+ });
+
+ test("returns true if aria-labelledby references both label and non-label elements", () => {
+ const customContext: TSESLint.RuleContext = {
+ getSourceCode: () => ({
+ getText: () => "Test Label
",
+ text: () => "Test Label
"
+ })
+ } as unknown as TSESLint.RuleContext;
+
+ openingElement.attributes = [createJSXAttribute("aria-labelledby", "existing-label-id")];
+ const result = hasAssociatedLabelViaAriaLabelledBy(openingElement, customContext);
+ expect(result).toBe(true);
+ });
+ });
+
+ describe("hasAssociatedLabelViaAriaDescribedby", () => {
+ let context: TSESLint.RuleContext;
+ let openingElement: TSESTree.JSXOpeningElement;
+
+ beforeEach(() => {
+ context = mockContext();
+ openingElement = {
+ attributes: []
+ } as unknown as TSESTree.JSXOpeningElement;
+ });
+
+ function createJSXAttribute(name: string, value: string | number | null): TSESTree.JSXAttribute {
+ return {
+ type: AST_NODE_TYPES.JSXAttribute,
+ name: { type: AST_NODE_TYPES.JSXIdentifier, name } as TSESTree.JSXIdentifier,
+ value: value !== null ? ({ type: AST_NODE_TYPES.Literal, value } as TSESTree.Literal) : null,
+ loc: {} as TSESTree.SourceLocation,
+ range: [0, 0]
+ };
+ }
+
+ test("returns false if aria-describedby is missing", () => {
+ const result = hasAssociatedLabelViaAriaDescribedby(openingElement, context);
+ expect(result).toBe(false);
+ });
+
+ test("returns false if aria-describedby is empty", () => {
+ openingElement.attributes = [createJSXAttribute("aria-describedby", "")];
+ const result = hasAssociatedLabelViaAriaDescribedby(openingElement, context);
+ expect(result).toBe(false);
+ });
+
+ test("returns false if aria-describedby value is not a string", () => {
+ openingElement.attributes = [createJSXAttribute("aria-describedby", 123)];
+ const result = hasAssociatedLabelViaAriaDescribedby(openingElement, context);
+ expect(result).toBe(false);
+ });
+
+ test("returns false if referenced element by id does not exist", () => {
+ const customContext: TSESLint.RuleContext = {
+ getSourceCode: () => ({
+ getText: () => "Test Label",
+ text: () => "Test Label"
+ })
+ } as unknown as TSESLint.RuleContext;
+
+ openingElement.attributes = [createJSXAttribute("aria-describedby", "non-existing-id")];
+ const result = hasAssociatedLabelViaAriaDescribedby(openingElement, customContext);
+ expect(result).toBe(false);
+ });
+
+ test("returns true if aria-describedby references an existing label element", () => {
+ const customContext: TSESLint.RuleContext = {
+ getSourceCode: () => ({
+ getText: () => "Test Label",
+ text: () => "Test Label"
+ })
+ } as unknown as TSESLint.RuleContext;
+
+ openingElement.attributes = [createJSXAttribute("aria-describedby", "existing-label-id")];
+ const result = hasAssociatedLabelViaAriaDescribedby(openingElement, customContext);
+ expect(result).toBe(true);
+ });
+
+ test("returns true if aria-describedby references an existing non-label element", () => {
+ const customContext: TSESLint.RuleContext = {
+ getSourceCode: () => ({
+ getText: () => "Test Label
",
+ text: () => "Test Label
"
+ })
+ } as unknown as TSESLint.RuleContext;
+
+ openingElement.attributes = [createJSXAttribute("aria-describedby", "existing-non-label-id")];
+ const result = hasAssociatedLabelViaAriaDescribedby(openingElement, customContext);
+ expect(result).toBe(true);
+ });
+
+ test("returns true if aria-describedby references both label and non-label elements", () => {
+ const customContext: TSESLint.RuleContext = {
+ getSourceCode: () => ({
+ getText: () => "Test Label
",
+ text: () => "Test Label
"
+ })
+ } as unknown as TSESLint.RuleContext;
+
+ openingElement.attributes = [createJSXAttribute("aria-describedby", "existing-label-id")];
+ const result = hasAssociatedLabelViaAriaDescribedby(openingElement, customContext);
+ expect(result).toBe(true);
+ });
+ });
+});
diff --git a/tests/lib/rules/visual-label-better-than-aria-suggestion.js b/tests/lib/rules/visual-label-better-than-aria-suggestion.test.ts
similarity index 73%
rename from tests/lib/rules/visual-label-better-than-aria-suggestion.js
rename to tests/lib/rules/visual-label-better-than-aria-suggestion.test.ts
index 8262ffc..1921d17 100644
--- a/tests/lib/rules/visual-label-better-than-aria-suggestion.js
+++ b/tests/lib/rules/visual-label-better-than-aria-suggestion.test.ts
@@ -1,31 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-"use strict";
+import { Rule } from "eslint";
+import ruleTester from "./helper/ruleTester";
+import rule from "../../../lib/rules/visual-label-better-than-aria-suggestion";
-const { applicableComponents } = require("../../../lib/applicableComponents/buttonBasedComponents");
+import { applicableComponents } from "../../../lib/applicableComponents/inputBasedComponents";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
-const RuleTester = require("eslint").RuleTester;
-
-const rule = require("../../../lib/rules/visual-label-better-than-aria-suggestion");
-
-RuleTester.setDefaultConfig({
- parserOptions: {
- ecmaVersion: 6,
- ecmaFeatures: {
- jsx: true
- }
- }
-});
-
//------------------------------------------------------------------------------
// Helper function to generate test cases
//------------------------------------------------------------------------------
-function generateTestCases(componentName) {
+const generateTestCases = (componentName: string) => {
return {
valid: [
`<>Some Label<${componentName} id="some-id"/>>`,
@@ -40,7 +29,7 @@ function generateTestCases(componentName) {
}
]
};
-}
+};
// Collect all test cases for all applicable components
const allTestCases = applicableComponents.flatMap(component => generateTestCases(component));
@@ -49,8 +38,7 @@ const allTestCases = applicableComponents.flatMap(component => generateTestCases
// Tests
//------------------------------------------------------------------------------
-const ruleTester = new RuleTester();
-ruleTester.run("visual-label-better-than-aria-suggestion", rule, {
+ruleTester.run("visual-label-better-than-aria-suggestion", rule as unknown as Rule.RuleModule, {
valid: allTestCases.flatMap(test => test.valid),
invalid: allTestCases.flatMap(test => test.invalid)
});
diff --git a/tsconfig.json b/tsconfig.json
index 6613001..8bde47e 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,8 +5,7 @@
"allowUnusedLabels": false,
"declaration": true,
"declarationMap": false,
- "module": "Node16",
- "moduleResolution": "Node16",
+ "module": "CommonJS",
"noImplicitReturns": true,
"pretty": true,
"resolveJsonModule": true,