Repository: ymrl/a11y-visualizer
Pull request #59 (2024-05-10 23:56:00)

🚀 Use selectors only categories turned on


Patch:
diff --git a/src/content/Root.tsx b/src/content/Root.tsx
index 6981062..4e6de99 100644
--- a/src/content/Root.tsx
+++ b/src/content/Root.tsx
@@ -78,6 +78,7 @@ export const Root = ({
           ...popoversRef.current,
           ...dialogsRef.current,
         ].filter((el): el is Element => !!el),
+        settings,
       );
 
       setMetaList(elements);
diff --git a/src/content/dom/collectElements.ts b/src/content/dom/collectElements.ts
index 08c550a..758b162 100644
--- a/src/content/dom/collectElements.ts
+++ b/src/content/dom/collectElements.ts
@@ -11,19 +11,30 @@ import { FormSelectors, formTips, isFormControl } from "./tips/formTips";
 import { imageTips, isImage, ImageSelectors } from "./tips/imageTips";
 import { computeAccessibleName } from "dom-accessibility-api";
 import { isAriaHidden } from "./isAriaHidden";
+import { CategorySettings } from "../../settings";
 
-const Selector = [
-  ...(ImageSelectors || []),
-  ...(FormSelectors || []),
-  ...(LinkSelectors || []),
-  ...(HeadingSelectors || []),
-  ...(ButtonSelectors || []),
-  ...(AriraHiddenSelectors || []),
-].join(",");
+const getSelector = (settings: Partial<CategorySettings>) => {
+  return [
+    ...(settings.image ? ImageSelectors : []),
+    ...(settings.formControl ? FormSelectors : []),
+    ...(settings.link ? LinkSelectors : []),
+    ...(settings.heading ? HeadingSelectors : []),
+    ...(settings.button ? ButtonSelectors : []),
+    ...(settings.ariaHidden ? AriraHiddenSelectors : []),
+  ].join(",");
+};
 
 export const collectElements = (
   root: Element,
   excludes: Element[],
+  settings: Partial<CategorySettings> = {
+    image: true,
+    formControl: true,
+    link: true,
+    button: true,
+    heading: true,
+    ariaHidden: true,
+  },
 ): {
   elements: ElementMeta[];
   rootWidth: number;
@@ -60,47 +71,51 @@ export const collectElements = (
   const offsetX = offsetPosition.x;
   const offsetY = offsetPosition.y;
 
+  const selector = getSelector(settings);
+
   return {
     rootHeight,
     rootWidth,
-    elements: [...root.querySelectorAll(Selector)]
-      .map((el: Element) => {
-        if (excludes.some((exclude: Element) => exclude.contains(el)))
-          return null;
-        const name = computeAccessibleName(el);
-        const nameTips: ElementTip[] = name
-          ? [{ type: "name", content: name }]
-          : [];
-        const images = imageTips(el);
-        const forms = formTips(el);
-        const buttons = buttonTips(el);
-        const links = linkTips(el);
-        const heading = headingTips(el);
-        const ariaHidden = ariaHiddenTips(el);
-        const global = globalTips(el);
-        return {
-          ...getElementPosition(el, w, offsetX, offsetY),
-          hidden: isHidden(el),
-          categories: [
-            isImage(el) ? "image" : "",
-            isFormControl(el) ? "formControl" : "",
-            isButton(el) ? "button" : "",
-            isLink(el) ? "link" : "",
-            isHeading(el) ? "heading" : "",
-            isAriaHidden(el) ? "ariaHidden" : "",
-          ].filter(Boolean) as Category[],
-          tips: [
-            ...heading,
-            ...nameTips,
-            ...images,
-            ...forms,
-            ...buttons,
-            ...links,
-            ...ariaHidden,
-            ...global,
-          ],
-        };
-      })
-      .filter((el): el is ElementMeta => el !== null),
+    elements: selector
+      ? [...root.querySelectorAll(getSelector(settings))]
+          .map((el: Element) => {
+            if (excludes.some((exclude: Element) => exclude.contains(el)))
+              return null;
+            const name = computeAccessibleName(el);
+            const nameTips: ElementTip[] = name
+              ? [{ type: "name", content: name }]
+              : [];
+            const images = imageTips(el);
+            const forms = formTips(el);
+            const buttons = buttonTips(el);
+            const links = linkTips(el);
+            const heading = headingTips(el);
+            const ariaHidden = ariaHiddenTips(el);
+            const global = globalTips(el);
+            return {
+              ...getElementPosition(el, w, offsetX, offsetY),
+              hidden: isHidden(el),
+              categories: [
+                isImage(el) ? "image" : "",
+                isFormControl(el) ? "formControl" : "",
+                isButton(el) ? "button" : "",
+                isLink(el) ? "link" : "",
+                isHeading(el) ? "heading" : "",
+                isAriaHidden(el) ? "ariaHidden" : "",
+              ].filter(Boolean) as Category[],
+              tips: [
+                ...heading,
+                ...nameTips,
+                ...images,
+                ...forms,
+                ...buttons,
+                ...links,
+                ...ariaHidden,
+                ...global,
+              ],
+            };
+          })
+          .filter((el): el is ElementMeta => el !== null)
+      : [],
   };
 };
diff --git a/src/settings/types.ts b/src/settings/types.ts
index e1585f2..e8e7738 100644
--- a/src/settings/types.ts
+++ b/src/settings/types.ts
@@ -1,18 +1,21 @@
-export type Settings = {
-  accessibilityInfo: boolean;
+export type CategorySettings = {
   image: boolean;
   formControl: boolean;
   link: boolean;
   button: boolean;
   heading: boolean;
   ariaHidden: boolean;
+};
+
+export type Settings = {
+  accessibilityInfo: boolean;
   interactiveMode: boolean;
   showLiveRegions: boolean;
   announcementMaxSeconds: number;
   announcementSecondsPerCharacter: number;
   tipOpacityPercent: number;
   liveRegionOpacityPercent: number;
-};
+} & CategorySettings;
 
 export type Message =
   | {


Test patch:
diff --git a/src/content/dom/collectElements.test.ts b/src/content/dom/collectElements.test.ts
index 3a13c52..9dd37eb 100644
--- a/src/content/dom/collectElements.test.ts
+++ b/src/content/dom/collectElements.test.ts
@@ -1,53 +1,82 @@
-import { JSDOM } from "jsdom";
-import { expect, test } from "vitest";
+import { afterEach, describe, expect, test } from "vitest";
 import { collectElements } from "./collectElements";
 
-test("image", () => {
-  const dom = new JSDOM(`<!DOCTYPE html>
-    <html>
-      <body>
-        <img id="noAlt" src="https://example.com/image.jpg">
-        <img id="emptyAlt" src="https://example.com/image.jpg" alt="">
-        <img id="alt" src="https://example.com/image.jpg" alt="image">
-        <span id="role-img" role="img" aria-label="image"></span>
-        <span id="role-img-empty" role="img"></span>
-      </body>
-    </html>
-  `);
-
-  const result = collectElements(dom.window.document.body, []);
-  expect(result.elements).toHaveLength(5);
-
-  const noAlt = dom.window.document.getElementById("noAlt");
-  expect(noAlt).not.toBeNull();
-  if (noAlt) {
+const setAttributes = (
+  element: Element,
+  attributes: Record<string, string>,
+) => {
+  for (const key in attributes) {
+    element.setAttribute(key, attributes[key]);
+  }
+};
+const appendChildren = (element: Element, children: Element[]) => {
+  children.forEach((child) => {
+    element.appendChild(child);
+  });
+};
+
+describe("collectElements()", () => {
+  afterEach(() => {
+    document.body.innerHTML = "";
+  });
+
+  test("image", () => {
+    const noAlt = document.createElement("img");
+    noAlt.src = "https://example.com/image.jpg";
+    const emptyAlt = document.createElement("img");
+    setAttributes(emptyAlt, {
+      src: "https://example.com/image.jpg",
+      alt: "",
+    });
+    const alt = document.createElement("img");
+    setAttributes(alt, {
+      src: "https://example.com/image.jpg",
+      alt: "image",
+    });
+    const roleImg = document.createElement("span");
+    setAttributes(roleImg, {
+      role: "img",
+      "aria-label": "image",
+    });
+    const roleImgEmpty = document.createElement("span");
+    roleImgEmpty.setAttribute("role", "img");
+    const notImage = document.createElement("span");
+    const exclude = document.createElement("div");
+    exclude.appendChild(document.createElement("img"));
+    appendChildren(document.body, [
+      noAlt,
+      emptyAlt,
+      alt,
+      roleImg,
+      roleImgEmpty,
+      notImage,
+      exclude,
+    ]);
+    const emptyResult = collectElements(document.body, [], {});
+    expect(emptyResult.elements).toHaveLength(0);
+
+    const result = collectElements(document.body, [exclude], { image: true });
+    expect(result.elements).toHaveLength(5);
+
+    //noAlt
     expect(result.elements[0].categories).toContain("image");
     expect(result.elements[0].tips).toHaveLength(1);
     expect(result.elements[0].tips[0].type).toBe("error");
     expect(result.elements[0].tips[0].content).toBe("messages.noAltImage");
-  }
 
-  const emptyAlt = dom.window.document.getElementById("emptyAlt");
-  expect(emptyAlt).not.toBeNull();
-  if (emptyAlt) {
+    // emptyAlt
     expect(result.elements[1].categories).toContain("image");
     expect(result.elements[1].tips).toHaveLength(1);
     expect(result.elements[1].tips[0].type).toBe("warning");
     expect(result.elements[1].tips[0].content).toBe("messages.emptyAltImage");
-  }
 
-  const alt = dom.window.document.getElementById("alt");
-  expect(alt).not.toBeNull();
-  if (alt) {
+    // alt
     expect(result.elements[2].categories).toContain("image");
     expect(result.elements[2].tips).toHaveLength(1);
     expect(result.elements[2].tips[0].type).toBe("name");
     expect(result.elements[2].tips[0].content).toBe("image");
-  }
 
-  const roleImg = dom.window.document.getElementById("role-img");
-  expect(roleImg).not.toBeNull();
-  if (roleImg) {
+    // roleImg
     expect(result.elements[3].categories).toContain("image");
     expect(
       result.elements[3].tips.find((tip) => tip.type === "name"),
@@ -67,11 +96,8 @@ test("image", () => {
     expect(
       result.elements[3].tips.find((tip) => tip.type === "role")?.content,
     ).toBe("img");
-  }
 
-  const roleImgEmpty = dom.window.document.getElementById("role-img-empty");
-  expect(roleImgEmpty).not.toBeNull();
-  if (roleImgEmpty) {
+    // roleImgEmpty
     expect(result.elements[4].categories).toContain("image");
     expect(
       result.elements[4].tips.find((tip) => tip.type === "name"),
@@ -94,5 +120,158 @@ test("image", () => {
     expect(
       result.elements[4].tips.find((tip) => tip.type === "role")?.content,
     ).toBe("img");
-  }
+  });
+
+  test("formControl", () => {
+    const noLabel = document.createElement("input");
+    const emptyLabel = document.createElement("input");
+    emptyLabel.setAttribute("aria-label", "");
+    const labeled = document.createElement("input");
+    labeled.id = "labeled";
+    const label = document.createElement("label");
+    label.htmlFor = "labeled";
+    label.innerHTML = "labeled";
+    const noFor = document.createElement("label");
+    const notFormControl = document.createElement("span");
+    appendChildren(document.body, [
+      noLabel,
+      emptyLabel,
+      labeled,
+      label,
+      noFor,
+      notFormControl,
+    ]);
+    const emptyResult = collectElements(document.body, [], {});
+    expect(emptyResult.elements).toHaveLength(0);
+    const result = collectElements(document.body, [], { formControl: true });
+    expect(result.elements).toHaveLength(5);
+    // noLabel
+    expect(result.elements[0].categories).toContain("formControl");
+    expect(result.elements[0].tips).toHaveLength(1);
+    expect(result.elements[0].tips[0].type).toBe("error");
+    expect(result.elements[0].tips[0].content).toBe("messages.noName");
+
+    // emptyLabel
+    expect(result.elements[1].categories).toContain("formControl");
+    expect(result.elements[1].tips).toHaveLength(1);
+    expect(result.elements[1].tips[0].type).toBe("error");
+    expect(result.elements[1].tips[0].content).toBe("messages.noName");
+
+    // labeled
+    expect(result.elements[2].categories).toContain("formControl");
+    expect(result.elements[2].tips).toHaveLength(1);
+    expect(result.elements[2].tips[0].type).toBe("name");
+    expect(result.elements[2].tips[0].content).toBe("labeled");
+
+    //label
+    expect(result.elements[3].categories).toContain("formControl");
+    expect(result.elements[3].tips).toHaveLength(0);
+
+    // noFor
+    expect(result.elements[4].categories).toContain("formControl");
+    expect(result.elements[4].tips).toHaveLength(1);
+    expect(result.elements[4].tips[0].type).toBe("warning");
+    expect(result.elements[4].tips[0].content).toBe(
+      "messages.noControlForLabel",
+    );
+  });
+
+  test("link", () => {
+    const noHref = document.createElement("a");
+    noHref.textContent = "link";
+    const href = document.createElement("a");
+    href.href = "https://example.com";
+    href.textContent = "link";
+    const roleLink = document.createElement("span");
+    setAttributes(roleLink, {
+      role: "link",
+      "aria-label": "link",
+    });
+    const roleLinkEmpty = document.createElement("span");
+    roleLinkEmpty.setAttribute("role", "link");
+    const notLink = document.createElement("span");
+    appendChildren(document.body, [
+      noHref,
+      href,
+      roleLink,
+      roleLinkEmpty,
+      notLink,
+    ]);
+    const emptyResult = collectElements(document.body, [], {});
+    expect(emptyResult.elements).toHaveLength(0);
+    const result = collectElements(document.body, [], { link: true });
+    expect(result.elements).toHaveLength(4);
+  });
+
+  test("button", () => {
+    const button = document.createElement("button");
+    button.textContent = "button";
+    const notButton = document.createElement("div");
+    notButton.textContent = "not button";
+    const roleButton = document.createElement("span");
+    roleButton.setAttribute("role", "button");
+    roleButton.textContent = "role button";
+    const inputButton = document.createElement("input");
+    setAttributes(inputButton, {
+      type: "button",
+      value: "input button",
+    });
+    const inputSubmit = document.createElement("input");
+    setAttributes(inputSubmit, {
+      type: "submit",
+      value: "input submit",
+    });
+    const inputReset = document.createElement("input");
+    setAttributes(inputReset, {
+      type: "reset",
+      value: "input reset",
+    });
+    const inputImage = document.createElement("input");
+    setAttributes(inputImage, {
+      type: "image",
+      alt: "input image",
+    });
+    appendChildren(document.body, [
+      button,
+      notButton,
+      roleButton,
+      inputButton,
+      inputSubmit,
+      inputReset,
+      inputImage,
+    ]);
+    const emptyResult = collectElements(document.body, [], {});
+    expect(emptyResult.elements).toHaveLength(0);
+    const result = collectElements(document.body, [], { button: true });
+    expect(result.elements).toHaveLength(6);
+  });
+
+  test("heading", () => {
+    const h1 = document.createElement("h1");
+    h1.textContent = "h1";
+    const roleHeading = document.createElement("span");
+    setAttributes(roleHeading, {
+      role: "heading",
+      "aria-level": "1",
+    });
+    const notHeading = document.createElement("div");
+    notHeading.textContent = "not heading";
+    appendChildren(document.body, [h1, roleHeading, notHeading]);
+    const emptyResult = collectElements(document.body, [], {});
+    expect(emptyResult.elements).toHaveLength(0);
+    const result = collectElements(document.body, [], { heading: true });
+    expect(result.elements).toHaveLength(2);
+  });
+
+  test("ariaHidden", () => {
+    const ariaHidden = document.createElement("div");
+    ariaHidden.setAttribute("aria-hidden", "true");
+    const notAriaHidden = document.createElement("div");
+    notAriaHidden.textContent = "not aria-hidden";
+    appendChildren(document.body, [ariaHidden, notAriaHidden]);
+    const emptyResult = collectElements(document.body, [], {});
+    expect(emptyResult.elements).toHaveLength(0);
+    const result = collectElements(document.body, [], { ariaHidden: true });
+    expect(result.elements).toHaveLength(1);
+  });
 });