Repository: ymrl/a11y-visualizer (ts)

Issue:
要素とロールが別カテゴリーだと、「名前」チップが重複することがある
たとえば `<input type="checkbox" role="button">` のように、フォームコントロールでありつつボタンでもあるような状態だと、「名前」チップが重複した状態になってしまう。これはWikipediaのドロップダウンボタンに存在する実装。

<img width="129" alt="「ツール」の下に、「名前」チップの「ツール」が2つある状態" src="https://github.com/ymrl/a11y-visualizer/assets/154188/257bedaf-03a9-4cfc-a023-ea3afc1611c1">

「名前」チップはそれぞれのカテゴリーで付ける・付けないの判断をしているが、重複しないようにしたい

Pull request description:
:bug: Fix overlapped name
fix #37 

Patch:
diff --git a/src/content/components/MetaInfo.tsx b/src/content/components/MetaInfo.tsx
index 769c5b8..380f932 100644
--- a/src/content/components/MetaInfo.tsx
+++ b/src/content/components/MetaInfo.tsx
@@ -80,19 +80,20 @@ export const MetaInfo = ({
         height,
       }}
     >
-      {categories
-        .filter((category) => settings[category])
-        .map((category, i) => (
-          <div
-            key={i}
-            style={{
-              position: "absolute",
-              inset: 0,
-              ...colors(category),
-              boxShadow: "0 0 0 1px #fff, inset 0 0 0 1px #fff",
-            }}
-          />
-        ))}
+      {tips.length > 0 &&
+        categories
+          .filter((category) => settings[category])
+          .map((category, i) => (
+            <div
+              key={i}
+              style={{
+                position: "absolute",
+                inset: 0,
+                ...colors(category),
+                boxShadow: "0 0 0 1px #fff, inset 0 0 0 1px #fff",
+              }}
+            />
+          ))}
       <div
         style={{
           position: "absolute",
diff --git a/src/content/dom/collectElements.ts b/src/content/dom/collectElements.ts
index d81bbe5..08c550a 100644
--- a/src/content/dom/collectElements.ts
+++ b/src/content/dom/collectElements.ts
@@ -1,14 +1,16 @@
-import { ElementMeta, Category } from "../types";
+import { ElementMeta, Category, ElementTip } from "../types";
 import { getPositionBaseElement } from "./getPositionBaseElement";
 import { isHidden } from "./isHidden";
 import { getElementPosition } from "./getElementPosition";
 import { globalTips } from "./tips/globalTips";
 import { AriraHiddenSelectors, ariaHiddenTips } from "./tips/ariaHiddenTips";
-import { HeadingSelectors, headingTips } from "./tips/headingTips";
-import { LinkSelectors, linkTips } from "./tips/linkTips";
-import { ButtonSelectors, buttonTips } from "./tips/buttonTips";
-import { FormSelectors, formTips } from "./tips/formTips";
+import { HeadingSelectors, headingTips, isHeading } from "./tips/headingTips";
+import { LinkSelectors, isLink, linkTips } from "./tips/linkTips";
+import { ButtonSelectors, buttonTips, isButton } from "./tips/buttonTips";
+import { FormSelectors, formTips, isFormControl } from "./tips/formTips";
 import { imageTips, isImage, ImageSelectors } from "./tips/imageTips";
+import { computeAccessibleName } from "dom-accessibility-api";
+import { isAriaHidden } from "./isAriaHidden";
 
 const Selector = [
   ...(ImageSelectors || []),
@@ -65,6 +67,10 @@ export const collectElements = (
       .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);
@@ -77,18 +83,19 @@ export const collectElements = (
           hidden: isHidden(el),
           categories: [
             isImage(el) ? "image" : "",
-            forms.length > 0 ? "formControl" : "",
-            buttons.length > 0 ? "button" : "",
-            links.length > 0 ? "link" : "",
-            heading.length > 0 ? "heading" : "",
-            ariaHidden.length > 0 ? "ariaHidden" : "",
+            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,
-            ...heading,
             ...ariaHidden,
             ...global,
           ],
diff --git a/src/content/dom/tips/buttonTips.ts b/src/content/dom/tips/buttonTips.ts
index 44ced19..f16af28 100644
--- a/src/content/dom/tips/buttonTips.ts
+++ b/src/content/dom/tips/buttonTips.ts
@@ -12,8 +12,7 @@ export const ButtonSelectors = [
   'input[type="image"]',
 ] as const;
 
-export const buttonTips = (el: Element): ElementTip[] => {
-  const result: ElementTip[] = [];
+export const isButton = (el: Element): boolean => {
   const tagName = el.tagName.toLowerCase();
   const typeAttr = el.getAttribute("type");
   const hasButtonTag =
@@ -22,12 +21,15 @@ export const buttonTips = (el: Element): ElementTip[] => {
       !!typeAttr &&
       ["button", "submit", "reset", "image"].includes(typeAttr));
   const hasButtonRole = el.getAttribute("role") === "button";
+  return hasButtonTag || hasButtonRole;
+};
+
+export const buttonTips = (el: Element): ElementTip[] => {
+  const result: ElementTip[] = [];
 
-  if (hasButtonTag || hasButtonRole) {
+  if (isButton(el)) {
     const name = computeAccessibleName(el);
-    if (name) {
-      result.push({ type: "name", content: name });
-    } else if (!isAriaHidden(el)) {
+    if (!name && !isAriaHidden(el)) {
       result.push({ type: "error", content: "messages.noName" });
     }
     if (!isFocusable(el)) {
diff --git a/src/content/dom/tips/formTips.ts b/src/content/dom/tips/formTips.ts
index 4282577..af6c2d9 100644
--- a/src/content/dom/tips/formTips.ts
+++ b/src/content/dom/tips/formTips.ts
@@ -29,22 +29,8 @@ const LABELABLE_SELECTOR = [
   "textarea",
 ].join(",");
 
-export const formTips = (el: Element): ElementTip[] => {
-  const result: ElementTip[] = [];
-  const tagName = el.tagName.toLowerCase();
-  const typeAttr = el.getAttribute("type");
-  const roleAttr = el.getAttribute("role") || "";
-  const hasInputTag =
-    (tagName === "input" &&
-      (typeAttr === null ||
-        (typeAttr &&
-          !["button", "submit", "reset", "image", "hidden"].includes(
-            typeAttr,
-          )))) ||
-    tagName === "textarea" ||
-    tagName === "select";
-
-  const hasInputRole = [
+const hasInputRole = (el: Element): boolean =>
+  [
     "checkbox",
     "combobox",
     "radio",
@@ -55,13 +41,38 @@ export const formTips = (el: Element): ElementTip[] => {
     "textbox",
     "menuitemcheckbox",
     "menuitemradio",
-  ].includes(roleAttr);
+  ].includes(el.getAttribute("role") || "");
+
+const hasInputTag = (el: Element): boolean => {
+  const tagName = el.tagName.toLowerCase();
+  const typeAttr = el.getAttribute("type");
+  return (
+    (tagName === "input" &&
+      (typeAttr === null ||
+        (typeAttr &&
+          !["button", "submit", "reset", "image", "hidden"].includes(
+            typeAttr,
+          )))) ||
+    tagName === "textarea" ||
+    tagName === "select"
+  );
+};
+
+const isLabel = (el: Element): boolean => el.tagName.toLowerCase() === "label";
+
+export const isFormControl = (el: Element): boolean =>
+  hasInputRole(el) || hasInputTag(el) || isLabel(el);
+
+export const formTips = (el: Element): ElementTip[] => {
+  const result: ElementTip[] = [];
+  const tagName = el.tagName.toLowerCase();
+  const typeAttr = el.getAttribute("type");
+  const hasTag = hasInputTag(el);
+  const hasRole = hasInputRole(el);
 
-  if (hasInputTag || hasInputRole) {
+  if (hasTag || hasRole) {
     const name = computeAccessibleName(el);
-    if (name) {
-      result.push({ type: "name", content: name });
-    } else if (!isAriaHidden(el)) {
+    if (!name && !isAriaHidden(el)) {
       result.push({ type: "error", content: "messages.noName" });
     }
     if (!isFocusable(el)) {
diff --git a/src/content/dom/tips/headingTips.ts b/src/content/dom/tips/headingTips.ts
index a70154c..168d413 100644
--- a/src/content/dom/tips/headingTips.ts
+++ b/src/content/dom/tips/headingTips.ts
@@ -12,17 +12,28 @@ export const HeadingSelectors = [
   '[role="heading"]',
 ] as const;
 
+const hasHeadingTag = (el: Element): boolean => {
+  const tagName = el.tagName.toLowerCase();
+  return ["h1", "h2", "h3", "h4", "h5", "h6"].includes(tagName);
+};
+
+const hasHeadingRole = (el: Element): boolean =>
+  el.getAttribute("role") === "heading";
+
+export const isHeading = (el: Element): boolean =>
+  hasHeadingRole(el) || hasHeadingTag(el);
+
 export const headingTips = (el: Element): ElementTip[] => {
   const result: ElementTip[] = [];
   const tagName = el.tagName.toLowerCase();
   const hidden = isAriaHidden(el);
-  const hasHeadingTag = ["h1", "h2", "h3", "h4", "h5", "h6"].includes(tagName);
-  const hasHeadingRole = el.getAttribute("role") === "heading";
+  const hasTag = hasHeadingTag(el);
+  const hasRole = hasHeadingRole(el);
 
-  if (hasHeadingTag) {
+  if (hasTag) {
     result.push({ type: "level", content: `${tagName.slice(1)}` });
   }
-  if (hasHeadingRole) {
+  if (hasRole) {
     const ariaLevel = el.getAttribute("aria-level");
     if (ariaLevel) {
       result.push({ type: "level", content: `${ariaLevel}` });
@@ -33,11 +44,9 @@ export const headingTips = (el: Element): ElementTip[] => {
       });
     }
   }
-  if (hasHeadingTag || hasHeadingRole) {
+  if (hasTag || hasRole) {
     const name = computeAccessibleName(el);
-    if (name) {
-      result.push({ type: "name", content: name });
-    } else if (!hidden) {
+    if (!name && !hidden) {
       result.push({ type: "error", content: "messages.noName" });
     }
   }
diff --git a/src/content/dom/tips/imageTips.ts b/src/content/dom/tips/imageTips.ts
index 511f062..0c49050 100644
--- a/src/content/dom/tips/imageTips.ts
+++ b/src/content/dom/tips/imageTips.ts
@@ -17,9 +17,7 @@ export const imageTips = (el: Element): ElementTip[] => {
   const roleAttr = el.getAttribute("role") || "";
   if (isImage(el)) {
     const name = computeAccessibleName(el);
-    if (name) {
-      result.push({ type: "name", content: name });
-    } else if (!isAriaHidden(el) && roleAttr !== "presentation") {
+    if (!name && !isAriaHidden(el) && roleAttr !== "presentation") {
       if (tagName === "img") {
         const hasAlt = el.hasAttribute("alt");
         if (hasAlt) {
diff --git a/src/content/dom/tips/linkTips.ts b/src/content/dom/tips/linkTips.ts
index ec3660d..db42e6f 100644
--- a/src/content/dom/tips/linkTips.ts
+++ b/src/content/dom/tips/linkTips.ts
@@ -5,28 +5,34 @@ import { isFocusable } from "../isFocusable";
 
 export const LinkSelectors = ["a", "area", '[role="link"]'] as const;
 
+export const isLink = (el: Element): boolean =>
+  hasLinkRole(el) || hasLinkTag(el);
+
+const hasLinkRole = (el: Element): boolean =>
+  el.getAttribute("role") === "link";
+
+const hasLinkTag = (el: Element): boolean =>
+  ["a", "area"].includes(el.tagName.toLowerCase());
+
 export const linkTips = (el: Element): ElementTip[] => {
   const result: ElementTip[] = [];
-  const tagName = el.tagName.toLowerCase();
   const hidden = isAriaHidden(el);
-  const hasLinkTag = ["a", "area"].includes(tagName);
-  const hasLinkRole = el.getAttribute("role") === "link";
+  const hasTag = hasLinkTag(el);
+  const hasRole = hasLinkRole(el);
   const href = el.getAttribute("href");
 
-  if (hasLinkTag || hasLinkRole) {
+  if (hasTag || hasRole) {
     const name = computeAccessibleName(el);
-    if (name) {
-      result.push({ type: "name", content: name });
-    } else if ((hasLinkRole || href) && !hidden) {
+    if (!name && (hasRole || href) && !hidden) {
       result.push({ type: "error", content: "messages.noName" });
     }
   }
-  if (hasLinkTag) {
+  if (hasTag) {
     if (!el.hasAttribute("href")) {
       result.push({ type: "warning", content: "messages.noHref" });
     }
   }
-  if (hasLinkRole && !isFocusable(el)) {
+  if (hasRole && !isFocusable(el)) {
     result.push({ type: "error", content: "messages.notFocusable" });
   }
   return result;


Test patch:
diff --git a/src/content/dom/tips/ariaHiddenTips.test.ts b/src/content/dom/tips/ariaHiddenTips.test.ts
index 464bebf..383b951 100644
--- a/src/content/dom/tips/ariaHiddenTips.test.ts
+++ b/src/content/dom/tips/ariaHiddenTips.test.ts
@@ -4,20 +4,22 @@ import { ariaHiddenTips } from "./ariaHiddenTips";
 describe("ariaHiddenTips()", () => {
   test("empty", () => {
     const element = document.createElement("div");
-    expect(ariaHiddenTips(element)).toEqual([]);
+    expect(ariaHiddenTips(element)).toHaveLength(0);
   });
 
   test("aria-hidden attribute", () => {
     const element = document.createElement("div");
     element.setAttribute("aria-hidden", "true");
-    expect(ariaHiddenTips(element)).toEqual([
-      { type: "warning", content: "messages.ariaHidden" },
-    ]);
+    expect(
+      ariaHiddenTips(element).find(
+        (t) => t.type === "warning" && t.content === "messages.ariaHidden",
+      ),
+    ).toBeDefined();
   });
 
   test("aria-hidden invalid", () => {
     const element = document.createElement("div");
     element.setAttribute("aria-hidden", "false");
-    expect(ariaHiddenTips(element)).toEqual([]);
+    expect(ariaHiddenTips(element)).toHaveLength(0);
   });
 });
diff --git a/src/content/dom/tips/buttonTips.test.ts b/src/content/dom/tips/buttonTips.test.ts
index 186625f..10fd67f 100644
--- a/src/content/dom/tips/buttonTips.test.ts
+++ b/src/content/dom/tips/buttonTips.test.ts
@@ -1,5 +1,53 @@
 import { afterEach, describe, expect, test } from "vitest";
-import { buttonTips } from "./buttonTips";
+import { buttonTips, isButton } from "./buttonTips";
+
+describe("isButton()", () => {
+  test("div", () => {
+    const element = document.createElement("div");
+    expect(isButton(element)).toBe(false);
+  });
+
+  test("button element", () => {
+    const element = document.createElement("button");
+    expect(isButton(element)).toBe(true);
+  });
+
+  test("input type = button", () => {
+    const element = document.createElement("input");
+    element.setAttribute("type", "button");
+    expect(isButton(element)).toBe(true);
+  });
+
+  test("input type = submit", () => {
+    const element = document.createElement("input");
+    element.setAttribute("type", "submit");
+    expect(isButton(element)).toBe(true);
+  });
+
+  test("input type = reset", () => {
+    const element = document.createElement("input");
+    element.setAttribute("type", "reset");
+    expect(isButton(element)).toBe(true);
+  });
+
+  test("input type = image", () => {
+    const element = document.createElement("input");
+    element.setAttribute("type", "image");
+    expect(isButton(element)).toBe(true);
+  });
+
+  test("input type = text", () => {
+    const element = document.createElement("input");
+    element.setAttribute("type", "text");
+    expect(isButton(element)).toBe(false);
+  });
+
+  test("role = button", () => {
+    const element = document.createElement("div");
+    element.setAttribute("role", "button");
+    expect(isButton(element)).toBe(true);
+  });
+});
 
 describe("buttonTips()", () => {
   afterEach(() => {
@@ -7,7 +55,7 @@ describe("buttonTips()", () => {
   });
   test("div", () => {
     const element = document.createElement("div");
-    expect(buttonTips(element)).toEqual([]);
+    expect(buttonTips(element)).toHaveLength(0);
   });
 
   test("empty button", () => {
@@ -15,11 +63,9 @@ describe("buttonTips()", () => {
     document.body.appendChild(element);
     const result = buttonTips(element);
     expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toBeUndefined();
-    expect(result.find((t) => t.type === "error")).toEqual({
-      type: "error",
-      content: "messages.noName",
-    });
+    expect(
+      result.find((t) => t.type === "error" && t.content === "messages.noName"),
+    ).toBeDefined();
   });
 
   test("button element", () => {
@@ -27,12 +73,7 @@ describe("buttonTips()", () => {
     element.textContent = "Hello";
     document.body.appendChild(element);
     const result = buttonTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Hello",
-    });
-    expect(result.find((t) => t.type === "error")).toBeUndefined();
+    expect(result).toHaveLength(0);
   });
 
   test("input type = button", () => {
@@ -41,11 +82,7 @@ describe("buttonTips()", () => {
     element.value = "Hello";
     document.body.appendChild(element);
     const result = buttonTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Hello",
-    });
+    expect(result).toHaveLength(0);
   });
 
   test("input type = button without name", () => {
@@ -54,11 +91,9 @@ describe("buttonTips()", () => {
     document.body.appendChild(element);
     const result = buttonTips(element);
     expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toBeUndefined();
-    expect(result.find((t) => t.type === "error")).toEqual({
-      type: "error",
-      content: "messages.noName",
-    });
+    expect(
+      result.find((t) => t.type === "error" && t.content === "messages.noName"),
+    ).toBeDefined();
   });
 
   test("input type = submit", () => {
@@ -67,11 +102,7 @@ describe("buttonTips()", () => {
     element.value = "Hello";
     document.body.appendChild(element);
     const result = buttonTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Hello",
-    });
+    expect(result).toHaveLength(0);
   });
 
   test("input type = reset", () => {
@@ -80,11 +111,7 @@ describe("buttonTips()", () => {
     element.value = "Hello";
     document.body.appendChild(element);
     const result = buttonTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Hello",
-    });
+    expect(result).toHaveLength(0);
   });
 
   test("input type = submit without value", () => {
@@ -92,8 +119,7 @@ describe("buttonTips()", () => {
     element.setAttribute("type", "submit");
     document.body.appendChild(element);
     const result = buttonTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).not.toBeUndefined;
+    expect(result).toHaveLength(0);
   });
 
   test("input type = reset without value", () => {
@@ -101,8 +127,7 @@ describe("buttonTips()", () => {
     element.setAttribute("type", "reset");
     document.body.appendChild(element);
     const result = buttonTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).not.toBeUndefined;
+    expect(result).toHaveLength(0);
   });
 
   test("input type = image", () => {
@@ -111,11 +136,7 @@ describe("buttonTips()", () => {
     element.setAttribute("alt", "Hello");
     document.body.appendChild(element);
     const result = buttonTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Hello",
-    });
+    expect(result).toHaveLength(0);
   });
 
   test("input type = image without alt", () => {
@@ -123,8 +144,7 @@ describe("buttonTips()", () => {
     element.setAttribute("type", "image");
     document.body.appendChild(element);
     const result = buttonTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).not.toBeUndefined;
+    expect(result).toHaveLength(0);
   });
 
   test("role = button", () => {
@@ -134,11 +154,7 @@ describe("buttonTips()", () => {
     element.tabIndex = 0;
     document.body.appendChild(element);
     const result = buttonTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Hello",
-    });
+    expect(result).toHaveLength(0);
   });
 
   test("role = button without name", () => {
@@ -148,11 +164,9 @@ describe("buttonTips()", () => {
     document.body.appendChild(element);
     const result = buttonTips(element);
     expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toBeUndefined();
-    expect(result.find((t) => t.type === "error")).toEqual({
-      type: "error",
-      content: "messages.noName",
-    });
+    expect(
+      result.find((t) => t.type === "error" && t.content === "messages.noName"),
+    ).toBeDefined();
   });
 
   test("role = button without tabindex", () => {
@@ -161,15 +175,12 @@ describe("buttonTips()", () => {
     element.textContent = "Hello";
     document.body.appendChild(element);
     const result = buttonTips(element);
-    expect(result).toHaveLength(2);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Hello",
-    });
-    expect(result.find((t) => t.type === "error")).toEqual({
-      type: "error",
-      content: "messages.notFocusable",
-    });
+    expect(result).toHaveLength(1);
+    expect(
+      result.find(
+        (t) => t.type === "error" && t.content === "messages.notFocusable",
+      ),
+    ).toBeDefined();
   });
 
   test("role = button without namae and tabindex", () => {
@@ -180,22 +191,22 @@ describe("buttonTips()", () => {
     expect(result).toHaveLength(2);
     expect(
       result.find((t) => t.type === "error" && t.content === "messages.noName"),
-    ).not.toBeUndefined();
+    ).toBeDefined();
     expect(
       result.find(
         (t) => t.type === "error" && t.content === "messages.notFocusable",
       ),
-    ).not.toBeUndefined();
+    ).toBeDefined();
   });
 
   test("role = button with aria-hidden", () => {
     const element = document.createElement("div");
     element.setAttribute("role", "button");
     element.setAttribute("aria-hidden", "true");
+    element.setAttribute("tabindex", "0");
     element.textContent = "Hello";
     document.body.appendChild(element);
     const result = buttonTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toBeUndefined();
+    expect(result).toHaveLength(0);
   });
 });
diff --git a/src/content/dom/tips/formTips.test.ts b/src/content/dom/tips/formTips.test.ts
index bf0187e..558aa37 100644
--- a/src/content/dom/tips/formTips.test.ts
+++ b/src/content/dom/tips/formTips.test.ts
@@ -1,5 +1,33 @@
 import { describe, test, expect, afterEach } from "vitest";
-import { formTips } from "./formTips";
+import { formTips, isFormControl } from "./formTips";
+
+describe("isFormControl()", () => {
+  test("div", () => {
+    const element = document.createElement("div");
+    expect(isFormControl(element)).toBe(false);
+  });
+
+  test("input", () => {
+    const element = document.createElement("input");
+    expect(isFormControl(element)).toBe(true);
+  });
+
+  test("input hidden", () => {
+    const element = document.createElement("input");
+    element.setAttribute("type", "hidden");
+    expect(isFormControl(element)).toBe(false);
+  });
+
+  test("textarea", () => {
+    const element = document.createElement("textarea");
+    expect(isFormControl(element)).toBe(true);
+  });
+
+  test("select", () => {
+    const element = document.createElement("select");
+    expect(isFormControl(element)).toBe(true);
+  });
+});
 
 describe("formTips()", () => {
   afterEach(() => {
@@ -7,7 +35,7 @@ describe("formTips()", () => {
   });
   test("div", () => {
     const element = document.createElement("div");
-    expect(formTips(element)).toEqual([]);
+    expect(formTips(element)).toHaveLength(0);
   });
 
   test("empty input", () => {
@@ -15,11 +43,9 @@ describe("formTips()", () => {
     document.body.appendChild(element);
     const result = formTips(element);
     expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toBeUndefined();
-    expect(result.find((t) => t.type === "error")).toEqual({
-      type: "error",
-      content: "messages.noName",
-    });
+    expect(
+      result.find((t) => t.type === "error" && t.content === "messages.noName"),
+    ).toBeDefined();
   });
 
   test("labeled input", () => {
@@ -31,11 +57,7 @@ describe("formTips()", () => {
     label.htmlFor = "name";
     document.body.appendChild(label);
     const result = formTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Name",
-    });
+    expect(result).toHaveLength(0);
   });
 
   test("input type = radio, noname", () => {
@@ -48,17 +70,14 @@ describe("formTips()", () => {
     label.htmlFor = "name";
     document.body.appendChild(label);
     const result = formTips(element);
-    expect(result).toHaveLength(2);
+    expect(result).toHaveLength(1);
     expect(
       result.find(
         (t) => t.type === "error" && t.content === "messages.noNameAttribute",
       ),
-    ).not.toBeUndefined;
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Name",
-    });
+    ).toBeDefined;
   });
+
   test("input type = radio, nogropup", () => {
     const element = document.createElement("input");
     element.setAttribute("type", "radio");
@@ -76,15 +95,12 @@ describe("formTips()", () => {
     label.htmlFor = "name1";
     document.body.appendChild(label);
     const result = formTips(element);
-    expect(result).toHaveLength(2);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Name1",
-    });
-    expect(result.find((t) => t.type === "error")).toEqual({
-      type: "error",
-      content: "messages.noRadioGroup",
-    });
+    expect(result).toHaveLength(1);
+    expect(
+      result.find(
+        (t) => t.type === "error" && t.content === "messages.noRadioGroup",
+      ),
+    ).toBeDefined();
   });
 
   test("input type = radio", () => {
@@ -104,11 +120,7 @@ describe("formTips()", () => {
     label.htmlFor = "name1";
     document.body.appendChild(label);
     const result = formTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Name1",
-    });
+    expect(result).toHaveLength(0);
   });
 
   test("label for visible input", () => {
@@ -134,10 +146,12 @@ describe("formTips()", () => {
     document.body.appendChild(label);
     const result = formTips(label);
     expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "warning")).toEqual({
-      type: "warning",
-      content: "messages.noControlForLabel",
-    });
+    expect(
+      result.find(
+        (t) =>
+          t.type === "warning" && t.content === "messages.noControlForLabel",
+      ),
+    ).toBeDefined();
   });
 
   test("label for no input", () => {
@@ -146,9 +160,11 @@ describe("formTips()", () => {
     document.body.appendChild(label);
     const result = formTips(label);
     expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "warning")).toEqual({
-      type: "warning",
-      content: "messages.noControlForLabel",
-    });
+    expect(
+      result.find(
+        (t) =>
+          t.type === "warning" && t.content === "messages.noControlForLabel",
+      ),
+    ).toBeDefined();
   });
 });
diff --git a/src/content/dom/tips/headingTips.test.ts b/src/content/dom/tips/headingTips.test.ts
index bc35fa5..0dafb31 100644
--- a/src/content/dom/tips/headingTips.test.ts
+++ b/src/content/dom/tips/headingTips.test.ts
@@ -1,5 +1,48 @@
 import { afterEach, describe, expect, test } from "vitest";
-import { headingTips } from "./headingTips";
+import { headingTips, isHeading } from "./headingTips";
+
+describe("isHeading()", () => {
+  test("div", () => {
+    const element = document.createElement("div");
+    expect(isHeading(element)).toBe(false);
+  });
+
+  test("h1", () => {
+    const element = document.createElement("h1");
+    expect(isHeading(element)).toBe(true);
+  });
+
+  test("h2", () => {
+    const element = document.createElement("h2");
+    expect(isHeading(element)).toBe(true);
+  });
+
+  test("h3", () => {
+    const element = document.createElement("h3");
+    expect(isHeading(element)).toBe(true);
+  });
+
+  test("h4", () => {
+    const element = document.createElement("h4");
+    expect(isHeading(element)).toBe(true);
+  });
+
+  test("h5", () => {
+    const element = document.createElement("h5");
+    expect(isHeading(element)).toBe(true);
+  });
+
+  test("h6", () => {
+    const element = document.createElement("h6");
+    expect(isHeading(element)).toBe(true);
+  });
+
+  test("role=heading", () => {
+    const element = document.createElement("div");
+    element.setAttribute("role", "heading");
+    expect(isHeading(element)).toBe(true);
+  });
+});
 
 describe("headingTips()", () => {
   afterEach(() => {
@@ -8,7 +51,7 @@ describe("headingTips()", () => {
 
   test("div", () => {
     const element = document.createElement("div");
-    expect(headingTips(element)).toEqual([]);
+    expect(headingTips(element)).toHaveLength(0);
   });
 
   test("empty h1", () => {
@@ -16,14 +59,12 @@ describe("headingTips()", () => {
     document.body.appendChild(element);
     const result = headingTips(element);
     expect(result).toHaveLength(2);
-    expect(result.find((t) => t.type === "level")).toEqual({
-      type: "level",
-      content: "1",
-    });
-    expect(result.find((t) => t.type === "error")).toEqual({
-      type: "error",
-      content: "messages.noName",
-    });
+    expect(
+      result.find((t) => t.type === "level" && t.content === "1"),
+    ).toBeDefined();
+    expect(
+      result.find((t) => t.type === "error" && t.content === "messages.noName"),
+    ).toBeDefined();
   });
 
   test("h1 with name", () => {
@@ -31,15 +72,10 @@ describe("headingTips()", () => {
     element.textContent = "Hello";
     document.body.appendChild(element);
     const result = headingTips(element);
-    expect(result).toHaveLength(2);
-    expect(result.find((t) => t.type === "level")).toEqual({
-      type: "level",
-      content: "1",
-    });
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Hello",
-    });
+    expect(result).toHaveLength(1);
+    expect(
+      result.find((t) => t.type === "level" && t.content === "1"),
+    ).toBeDefined();
   });
 
   test("levels", () => {
@@ -75,14 +111,12 @@ describe("headingTips()", () => {
     element.setAttribute("aria-level", "2");
     document.body.appendChild(element);
     const result = headingTips(element);
-    expect(result.find((t) => t.type === "level")).toEqual({
-      type: "level",
-      content: "2",
-    });
-    expect(result.find((t) => t.type === "error")).toEqual({
-      type: "error",
-      content: "messages.noName",
-    });
+    expect(
+      result.find((t) => t.type === "level" && t.content === "2"),
+    ).toBeDefined();
+    expect(
+      result.find((t) => t.type === "error" && t.content === "messages.noName"),
+    ).toBeDefined;
   });
 
   test("no aria-level", () => {
@@ -90,10 +124,11 @@ describe("headingTips()", () => {
     element.setAttribute("role", "heading");
     document.body.appendChild(element);
     const result = headingTips(element);
-    expect(result.find((t) => t.type === "error")).toEqual({
-      type: "error",
-      content: "messages.noHeadingLevel",
-    });
+    expect(
+      result.find(
+        (t) => t.type === "error" && t.content === "messages.noHeadingLevel",
+      ),
+    ).toBeDefined();
   });
 
   test("aria-level, name", () => {
@@ -103,13 +138,8 @@ describe("headingTips()", () => {
     element.textContent = "Hello";
     document.body.appendChild(element);
     const result = headingTips(element);
-    expect(result.find((t) => t.type === "level")).toEqual({
-      type: "level",
-      content: "2",
-    });
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Hello",
-    });
+    expect(
+      result.find((t) => t.type === "level" && t.content === "2"),
+    ).toBeDefined();
   });
 });
diff --git a/src/content/dom/tips/imageTips.test.ts b/src/content/dom/tips/imageTips.test.ts
index 086ad19..5889c83 100644
--- a/src/content/dom/tips/imageTips.test.ts
+++ b/src/content/dom/tips/imageTips.test.ts
@@ -38,14 +38,14 @@ describe("imageTips()", () => {
   test("div", () => {
     const element = document.createElement("div");
     document.body.appendChild(element);
-    expect(imageTips(element)).toEqual([]);
+    expect(imageTips(element)).toHaveLength(0);
   });
   test("img with alt", () => {
     const element = document.createElement("img");
     element.setAttribute("alt", "Hello");
     element.setAttribute("src", "hello.png");
     document.body.appendChild(element);
-    expect(imageTips(element)).toEqual([{ type: "name", content: "Hello" }]);
+    expect(imageTips(element)).toHaveLength(0);
   });
 
   test("img without alt", () => {
@@ -75,7 +75,6 @@ describe("imageTips()", () => {
       type: "tagName",
       content: "svg",
     });
-    expect(result.find((e) => e.type === "name")).toBeUndefined();
     expect(result.find((e) => e.type === "error")).toEqual({
       type: "error",
       content: "messages.noName",
diff --git a/src/content/dom/tips/linkTips.test.ts b/src/content/dom/tips/linkTips.test.ts
index 56c12d9..6ace05d 100644
--- a/src/content/dom/tips/linkTips.test.ts
+++ b/src/content/dom/tips/linkTips.test.ts
@@ -1,5 +1,25 @@
 import { afterEach, describe, expect, test } from "vitest";
-import { linkTips } from "./linkTips";
+import { linkTips, isLink } from "./linkTips";
+
+describe("isLink()", () => {
+  test("div", () => {
+    const element = document.createElement("div");
+    expect(isLink(element)).toBe(false);
+  });
+  test("a", () => {
+    const element = document.createElement("a");
+    expect(isLink(element)).toBe(true);
+  });
+  test("area", () => {
+    const element = document.createElement("area");
+    expect(isLink(element)).toBe(true);
+  });
+  test("role=link", () => {
+    const element = document.createElement("div");
+    element.setAttribute("role", "link");
+    expect(isLink(element)).toBe(true);
+  });
+});
 
 describe("linkTips()", () => {
   afterEach(() => {
@@ -16,7 +36,6 @@ describe("linkTips()", () => {
     document.body.appendChild(element);
     const result = linkTips(element);
     expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toBeUndefined();
     expect(result.find((t) => t.type === "warning")).toEqual({
       type: "warning",
       content: "messages.noHref",
@@ -29,12 +48,7 @@ describe("linkTips()", () => {
     element.href = "https://example.com";
     document.body.appendChild(element);
     const result = linkTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Hello",
-    });
-    expect(result.find((t) => t.type === "error")).toBeUndefined();
+    expect(result).toHaveLength(0);
   });
 
   test("a without href", () => {
@@ -54,7 +68,6 @@ describe("linkTips()", () => {
     document.body.appendChild(element);
     const result = linkTips(element);
     expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toBeUndefined();
     expect(result.find((t) => t.type === "error")).toEqual({
       type: "error",
       content: "messages.noName",
@@ -69,11 +82,7 @@ describe("linkTips()", () => {
     document.body.appendChild(mapElement);
     mapElement.appendChild(element);
     const result = linkTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Hello",
-    });
+    expect(result).toHaveLength(0);
   });
 
   test("area without href", () => {
@@ -83,11 +92,7 @@ describe("linkTips()", () => {
     document.body.appendChild(mapElement);
     mapElement.appendChild(element);
     const result = linkTips(element);
-    expect(result).toHaveLength(2);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Hello",
-    });
+    expect(result).toHaveLength(1);
     expect(result.find((t) => t.type === "warning")).toEqual({
       type: "warning",
       content: "messages.noHref",
@@ -101,11 +106,7 @@ describe("linkTips()", () => {
     element.textContent = "Hello";
     element.tabIndex = 0;
     const result = linkTips(element);
-    expect(result).toHaveLength(1);
-    expect(result.find((t) => t.type === "name")).toEqual({
-      type: "name",
-      content: "Hello",
-    });
+    expect(result).toHaveLength(0);
   });
 
   test("invalid role = link", () => {