-
Notifications
You must be signed in to change notification settings - Fork 3
feat: to-be-checked and to-be-partially-checked implementation #175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<T extends Element> extends Assertion<T> { | ||
|
|
@@ -434,6 +434,88 @@ export class ElementAssertion<T extends Element> extends Assertion<T> { | |
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Asserts that the element is checked. | ||
| * | ||
| * Valid for `<input type="checkbox">`, `<input type="radio">`, 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") ?? ""); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also this accepts any element with aria-checked, so a non-widget like would pass. Similar to how toBePressed gates on isButtonElement, we should require a checkable role (checkbox, radio, switch, …) here. Related: the aria-radio-* fixtures are currently unused, so radio-role coverage is missing too. |
||
| if (!isNativeCheckable && !isAriaCheckable) { | ||
| throw new Error( | ||
| "Only checkbox/radio inputs or valid aria-checked elements 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 `<input type="checkbox">` 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 checkbox inputs or checkbox-role elements 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. | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import type { ReactElement } from "react"; | ||
|
|
||
| export function CheckedTestComponent(): ReactElement { | ||
| return ( | ||
| <div> | ||
| {/* Native checkbox variants */} | ||
| <input data-testid="checkbox-checked" type="checkbox" defaultChecked={true} /> | ||
| <input data-testid="checkbox-unchecked" type="checkbox" /> | ||
|
|
||
| {/* Native radio variants */} | ||
| <input data-testid="radio-checked" type="radio" defaultChecked={true} /> | ||
| <input data-testid="radio-unchecked" type="radio" /> | ||
|
|
||
| {/* ARIA checkbox variants */} | ||
| <div data-testid="aria-checkbox-checked" role="checkbox" aria-checked="true">{"Checked"}</div> | ||
| <div data-testid="aria-checkbox-unchecked" role="checkbox" aria-checked="false">{"Unchecked"}</div> | ||
| <div data-testid="aria-checkbox-mixed" role="checkbox" aria-checked="mixed">{"Mixed"}</div> | ||
|
|
||
| {/* ARIA radio variants */} | ||
| <div data-testid="aria-radio-checked" role="radio" aria-checked="true">{"Checked"}</div> | ||
| <div data-testid="aria-radio-unchecked" role="radio" aria-checked="false">{"Unchecked"}</div> | ||
|
|
||
| {/* ARIA switch variants */} | ||
| <div data-testid="aria-switch-checked" role="switch" aria-checked="true">{"On"}</div> | ||
| <div data-testid="aria-switch-unchecked" role="switch" aria-checked="false">{"Off"}</div> | ||
|
|
||
| {/* Element for indeterminate test (set programmatically in test) */} | ||
| <input data-testid="checkbox-indeterminate" type="checkbox" /> | ||
|
|
||
| {/* Invalid elements */} | ||
| <div data-testid="non-checkable-element">{"Not checkable"}</div> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isValidAriaChecked already restricts to "true" | "false" | "mixed", so the ["true", "false"].includes(...) clause makes the helper call redundant — the second check fully covers it. Let's drop one of them (or fold the true/false-only logic into the helper) so the intent is clear.