From efb7ad92ee7dadd29f94cd114a1a0170f9d8c199 Mon Sep 17 00:00:00 2001 From: kmunoz Date: Tue, 7 Jul 2026 14:44:27 -0500 Subject: [PATCH 1/2] feat: to-be-checked and to-be-partially-checked implementation --- packages/dom/src/lib/ElementAssertion.ts | 86 ++++++- packages/dom/src/lib/helpers/accessibility.ts | 5 + packages/dom/src/lib/helpers/dom.ts | 12 + .../test/unit/lib/ElementAssertion.test.tsx | 228 ++++++++++++++++++ .../lib/fixtures/CheckedTestComponent.tsx | 34 +++ 5 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 packages/dom/test/unit/lib/fixtures/CheckedTestComponent.tsx diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index 21b1faf..83455bb 100644 --- a/packages/dom/src/lib/ElementAssertion.ts +++ b/packages/dom/src/lib/ElementAssertion.ts @@ -1,8 +1,8 @@ import { Assertion, AssertionError } from "@assertive-ts/core"; import equal from "fast-deep-equal"; -import { getAccessibleDescription, isValidAriaPressed } from "./helpers/accessibility"; -import { isButtonElement, isElementEmpty } from "./helpers/dom"; +import { getAccessibleDescription, isValidAriaChecked, isValidAriaPressed } from "./helpers/accessibility"; +import { isButtonElement, isCheckableInput, isCheckboxInput, isElementEmpty } from "./helpers/dom"; import { getExpectedAndReceivedStyles } from "./helpers/styles"; export class ElementAssertion extends Assertion { @@ -434,6 +434,88 @@ export class ElementAssertion extends Assertion { }); } + /** + * Asserts that the element is checked. + * + * Valid for ``, ``, or elements + * with a valid `aria-checked` attribute ("true" or "false"). + * + * @example + * // Native checkbox + * expect(element).toBeChecked(); + * expect(element).not.toBeChecked(); + * + * // ARIA checkbox + * expect(element).toBeChecked(); // when aria-checked="true" + * + * @returns the assertion instance. + */ + public toBeChecked(): this { + const isNativeCheckable = isCheckableInput(this.actual); + const isAriaCheckable = isValidAriaChecked(this.actual) + && ["true", "false"].includes(this.actual.getAttribute("aria-checked") ?? ""); + if (!isNativeCheckable && !isAriaCheckable) { + throw new Error( + 'Only inputs with type="checkbox" or type="radio" or elements with a valid aria-checked attribute can be used with .toBeChecked()', + ); + } + const isChecked = isNativeCheckable + ? (this.actual as unknown as HTMLInputElement).checked + : this.actual.getAttribute("aria-checked") === "true"; + const error = new AssertionError({ + actual: this.actual, + message: "Expected the element to be checked", + }); + const invertedError = new AssertionError({ + actual: this.actual, + message: "Expected the element to NOT be checked", + }); + return this.execute({ + assertWhen: isChecked, + error, + invertedError, + }); + } + + /** + * Asserts that the element is partially checked (indeterminate). + * + * Valid for `` or elements with `role="checkbox"` + * and `aria-checked="mixed"`. + * + * @example + * expect(element).toBePartiallyChecked(); + * expect(element).not.toBePartiallyChecked(); + * + * @returns the assertion instance. + */ + public toBePartiallyChecked(): this { + const isNativeCheckbox = isCheckboxInput(this.actual); + const isAriaCheckbox = this.actual.getAttribute("role") === "checkbox"; + if (!isNativeCheckbox && !isAriaCheckbox) { + throw new Error( + 'Only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with .toBePartiallyChecked()', + ); + } + const isPartiallyChecked = isNativeCheckbox + ? (this.actual as unknown as HTMLInputElement).indeterminate + || this.actual.getAttribute("aria-checked") === "mixed" + : this.actual.getAttribute("aria-checked") === "mixed"; + const error = new AssertionError({ + actual: this.actual, + message: "Expected the element to be partially checked", + }); + const invertedError = new AssertionError({ + actual: this.actual, + message: "Expected the element to NOT be partially checked", + }); + return this.execute({ + assertWhen: isPartiallyChecked, + error, + invertedError, + }); + } + /** * Helper method to assert the presence or absence of class names. * diff --git a/packages/dom/src/lib/helpers/accessibility.ts b/packages/dom/src/lib/helpers/accessibility.ts index b3e3f7a..f5738a9 100644 --- a/packages/dom/src/lib/helpers/accessibility.ts +++ b/packages/dom/src/lib/helpers/accessibility.ts @@ -33,3 +33,8 @@ export function isValidAriaPressed(element: Element): boolean { const pressedAttribute = element.getAttribute("aria-pressed"); return pressedAttribute !== null && ["true", "false", "mixed"].includes(pressedAttribute); } + +export function isValidAriaChecked(element: Element): boolean { + const checkedAttribute = element.getAttribute("aria-checked"); + return checkedAttribute !== null && ["true", "false", "mixed"].includes(checkedAttribute); +} diff --git a/packages/dom/src/lib/helpers/dom.ts b/packages/dom/src/lib/helpers/dom.ts index 3e1fb90..1175d1e 100644 --- a/packages/dom/src/lib/helpers/dom.ts +++ b/packages/dom/src/lib/helpers/dom.ts @@ -18,3 +18,15 @@ export function isButtonElement(element: Element): boolean { return isNativeButton || hasButtonRole; } + +export function isCheckableInput(element: Element): boolean { + const tagName = element.tagName.toLowerCase(); + const type = element.getAttribute("type"); + return tagName === "input" && (type === "checkbox" || type === "radio"); +} + +export function isCheckboxInput(element: Element): boolean { + const tagName = element.tagName.toLowerCase(); + const type = element.getAttribute("type"); + return tagName === "input" && type === "checkbox"; +} diff --git a/packages/dom/test/unit/lib/ElementAssertion.test.tsx b/packages/dom/test/unit/lib/ElementAssertion.test.tsx index ec746b2..77d4389 100644 --- a/packages/dom/test/unit/lib/ElementAssertion.test.tsx +++ b/packages/dom/test/unit/lib/ElementAssertion.test.tsx @@ -3,6 +3,7 @@ import { render } from "@testing-library/react"; import { ElementAssertion } from "../../../src/lib/ElementAssertion"; +import { CheckedTestComponent } from "./fixtures/CheckedTestComponent"; import { HaveClassTest } from "./fixtures/HaveClassTest"; import { NestedElementsTest } from "./fixtures/NestedElementsTest"; import { PressedTestComponent } from "./fixtures/PressedTestComponent"; @@ -823,4 +824,231 @@ describe("[Unit] ElementAssertion.test.ts", () => { }); }); }); + + describe(".toBeChecked", () => { + context("when the element is a native checkbox", () => { + context("when the checkbox is checked", () => { + it("returns the assertion instance", () => { + const { getByTestId } = render(); + const checkbox = getByTestId("checkbox-checked"); + const test = new ElementAssertion(checkbox); + + expect(test.toBeChecked()).toBeEqual(test); + + expect(() => test.not.toBeChecked()) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to NOT be checked"); + }); + }); + + context("when the checkbox is not checked", () => { + it("throws an assertion error", () => { + const { getByTestId } = render(); + const checkbox = getByTestId("checkbox-unchecked"); + const test = new ElementAssertion(checkbox); + + expect(() => test.toBeChecked()) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to be checked"); + + expect(test.not.toBeChecked()).toBeEqual(test); + }); + }); + }); + + context("when the element is a native radio", () => { + context("when the radio is checked", () => { + it("returns the assertion instance", () => { + const { getByTestId } = render(); + const radio = getByTestId("radio-checked"); + const test = new ElementAssertion(radio); + + expect(test.toBeChecked()).toBeEqual(test); + + expect(() => test.not.toBeChecked()) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to NOT be checked"); + }); + }); + + context("when the radio is not checked", () => { + it("throws an assertion error", () => { + const { getByTestId } = render(); + const radio = getByTestId("radio-unchecked"); + const test = new ElementAssertion(radio); + + expect(() => test.toBeChecked()) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to be checked"); + + expect(test.not.toBeChecked()).toBeEqual(test); + }); + }); + }); + + context("when the element has role=\"checkbox\" with aria-checked", () => { + context("when aria-checked is \"true\"", () => { + it("returns the assertion instance", () => { + const { getByTestId } = render(); + const div = getByTestId("aria-checkbox-checked"); + const test = new ElementAssertion(div); + + expect(test.toBeChecked()).toBeEqual(test); + + expect(() => test.not.toBeChecked()) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to NOT be checked"); + }); + }); + + context("when aria-checked is \"false\"", () => { + it("throws an assertion error", () => { + const { getByTestId } = render(); + const div = getByTestId("aria-checkbox-unchecked"); + const test = new ElementAssertion(div); + + expect(() => test.toBeChecked()) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to be checked"); + + expect(test.not.toBeChecked()).toBeEqual(test); + }); + }); + }); + + context("when the element has role=\"switch\" with aria-checked", () => { + context("when aria-checked is \"true\"", () => { + it("returns the assertion instance", () => { + const { getByTestId } = render(); + const div = getByTestId("aria-switch-checked"); + const test = new ElementAssertion(div); + + expect(test.toBeChecked()).toBeEqual(test); + + expect(() => test.not.toBeChecked()) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to NOT be checked"); + }); + }); + + context("when aria-checked is \"false\"", () => { + it("throws an assertion error", () => { + const { getByTestId } = render(); + const div = getByTestId("aria-switch-unchecked"); + const test = new ElementAssertion(div); + + expect(() => test.toBeChecked()) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to be checked"); + + expect(test.not.toBeChecked()).toBeEqual(test); + }); + }); + }); + + context("when the element is not a valid checkable element", () => { + it("throws a plain Error", () => { + const { getByTestId } = render(); + const div = getByTestId("non-checkable-element"); + const test = new ElementAssertion(div); + + expect(() => test.toBeChecked()).toThrowError(Error); + }); + }); + }); + + describe(".toBePartiallyChecked", () => { + context("when the element is a native checkbox", () => { + context("when the checkbox is indeterminate", () => { + it("returns the assertion instance", () => { + const { getByTestId } = render(); + const checkbox = getByTestId("checkbox-indeterminate") as HTMLInputElement; + checkbox.indeterminate = true; + const test = new ElementAssertion(checkbox); + + expect(test.toBePartiallyChecked()).toBeEqual(test); + + expect(() => test.not.toBePartiallyChecked()) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to NOT be partially checked"); + }); + }); + + context("when the checkbox is not indeterminate", () => { + it("throws an assertion error", () => { + const { getByTestId } = render(); + const checkbox = getByTestId("checkbox-checked"); + const test = new ElementAssertion(checkbox); + + expect(() => test.toBePartiallyChecked()) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to be partially checked"); + + expect(test.not.toBePartiallyChecked()).toBeEqual(test); + }); + }); + }); + + context("when the element has role=\"checkbox\" with aria-checked", () => { + context("when aria-checked is \"mixed\"", () => { + it("returns the assertion instance", () => { + const { getByTestId } = render(); + const div = getByTestId("aria-checkbox-mixed"); + const test = new ElementAssertion(div); + + expect(test.toBePartiallyChecked()).toBeEqual(test); + + expect(() => test.not.toBePartiallyChecked()) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to NOT be partially checked"); + }); + }); + + context("when aria-checked is \"true\"", () => { + it("throws an assertion error", () => { + const { getByTestId } = render(); + const div = getByTestId("aria-checkbox-checked"); + const test = new ElementAssertion(div); + + expect(() => test.toBePartiallyChecked()) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to be partially checked"); + + expect(test.not.toBePartiallyChecked()).toBeEqual(test); + }); + }); + + context("when aria-checked is \"false\"", () => { + it("throws an assertion error", () => { + const { getByTestId } = render(); + const div = getByTestId("aria-checkbox-unchecked"); + const test = new ElementAssertion(div); + + expect(() => test.toBePartiallyChecked()) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to be partially checked"); + + expect(test.not.toBePartiallyChecked()).toBeEqual(test); + }); + }); + }); + + context("when the element is not a valid checkbox element", () => { + it("throws a plain Error for a plain div", () => { + const { getByTestId } = render(); + const div = getByTestId("non-checkable-element"); + const test = new ElementAssertion(div); + + expect(() => test.toBePartiallyChecked()).toThrowError(Error); + }); + + it("throws a plain Error for a radio input", () => { + const { getByTestId } = render(); + const radio = getByTestId("radio-checked"); + const test = new ElementAssertion(radio); + + expect(() => test.toBePartiallyChecked()).toThrowError(Error); + }); + }); + }); }); diff --git a/packages/dom/test/unit/lib/fixtures/CheckedTestComponent.tsx b/packages/dom/test/unit/lib/fixtures/CheckedTestComponent.tsx new file mode 100644 index 0000000..23e26a8 --- /dev/null +++ b/packages/dom/test/unit/lib/fixtures/CheckedTestComponent.tsx @@ -0,0 +1,34 @@ +import type { ReactElement } from "react"; + +export function CheckedTestComponent(): ReactElement { + return ( +
+ {/* Native checkbox variants */} + + + + {/* Native radio variants */} + + + + {/* ARIA checkbox variants */} +
{"Checked"}
+
{"Unchecked"}
+
{"Mixed"}
+ + {/* ARIA radio variants */} +
{"Checked"}
+
{"Unchecked"}
+ + {/* ARIA switch variants */} +
{"On"}
+
{"Off"}
+ + {/* Element for indeterminate test (set programmatically in test) */} + + + {/* Invalid elements */} +
{"Not checkable"}
+
+ ); +} From be8430e21bd7dbd52cc3e5a71dc1fce835c7a629 Mon Sep 17 00:00:00 2001 From: kmunoz Date: Tue, 7 Jul 2026 15:06:46 -0500 Subject: [PATCH 2/2] fix: lint fixes --- packages/dom/src/lib/ElementAssertion.ts | 34 +++++++++---------- .../lib/fixtures/CheckedTestComponent.tsx | 4 +-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index 83455bb..48f69cf 100644 --- a/packages/dom/src/lib/ElementAssertion.ts +++ b/packages/dom/src/lib/ElementAssertion.ts @@ -435,28 +435,28 @@ export class ElementAssertion extends Assertion { } /** - * Asserts that the element is checked. - * - * Valid for ``, ``, or elements - * with a valid `aria-checked` attribute ("true" or "false"). - * - * @example - * // Native checkbox - * expect(element).toBeChecked(); - * expect(element).not.toBeChecked(); - * - * // ARIA checkbox - * expect(element).toBeChecked(); // when aria-checked="true" - * - * @returns the assertion instance. - */ + * Asserts that the element is checked. + * + * Valid for ``, ``, or elements + * with a valid `aria-checked` attribute ("true" or "false"). + * + * @example + * // Native checkbox + * expect(element).toBeChecked(); + * expect(element).not.toBeChecked(); + * + * // ARIA checkbox + * expect(element).toBeChecked(); // when aria-checked="true" + * + * @returns the assertion instance. + */ public toBeChecked(): this { const isNativeCheckable = isCheckableInput(this.actual); const isAriaCheckable = isValidAriaChecked(this.actual) && ["true", "false"].includes(this.actual.getAttribute("aria-checked") ?? ""); if (!isNativeCheckable && !isAriaCheckable) { throw new Error( - 'Only inputs with type="checkbox" or type="radio" or elements with a valid aria-checked attribute can be used with .toBeChecked()', + "Only checkbox/radio inputs or valid aria-checked elements can be used with .toBeChecked()", ); } const isChecked = isNativeCheckable @@ -494,7 +494,7 @@ export class ElementAssertion extends Assertion { const isAriaCheckbox = this.actual.getAttribute("role") === "checkbox"; if (!isNativeCheckbox && !isAriaCheckbox) { throw new Error( - 'Only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with .toBePartiallyChecked()', + "Only checkbox inputs or checkbox-role elements can be used with .toBePartiallyChecked()", ); } const isPartiallyChecked = isNativeCheckbox diff --git a/packages/dom/test/unit/lib/fixtures/CheckedTestComponent.tsx b/packages/dom/test/unit/lib/fixtures/CheckedTestComponent.tsx index 23e26a8..23a776e 100644 --- a/packages/dom/test/unit/lib/fixtures/CheckedTestComponent.tsx +++ b/packages/dom/test/unit/lib/fixtures/CheckedTestComponent.tsx @@ -4,11 +4,11 @@ export function CheckedTestComponent(): ReactElement { return (
{/* Native checkbox variants */} - + {/* Native radio variants */} - + {/* ARIA checkbox variants */}