Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 84 additions & 2 deletions packages/dom/src/lib/ElementAssertion.ts
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, isValidAriaCheckedStrict, 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> {
Expand Down Expand Up @@ -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") and
* a checkable role (`checkbox`, `radio`, or `switch`).
*
* @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 = isValidAriaCheckedStrict(this.actual);
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.
*
Expand Down
8 changes: 8 additions & 0 deletions packages/dom/src/lib/helpers/accessibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,11 @@ export function isValidAriaPressed(element: Element): boolean {
const pressedAttribute = element.getAttribute("aria-pressed");
return pressedAttribute !== null && ["true", "false", "mixed"].includes(pressedAttribute);
}

export function isValidAriaCheckedStrict(element: Element): boolean {
const checkedAttribute = element.getAttribute("aria-checked");
const role = element.getAttribute("role") ?? "";
return ["checkbox", "radio", "switch"].includes(role)
&& checkedAttribute !== null
&& ["true", "false"].includes(checkedAttribute);
}
12 changes: 12 additions & 0 deletions packages/dom/src/lib/helpers/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Loading