diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index 974d984..bb6d9f9 100644 --- a/packages/dom/src/lib/ElementAssertion.ts +++ b/packages/dom/src/lib/ElementAssertion.ts @@ -2,7 +2,7 @@ import { Assertion, AssertionError } from "@assertive-ts/core"; import equal from "fast-deep-equal"; import { getAccessibleDescription } from "./helpers/accessibility"; -import { isElementEmpty } from "./helpers/dom"; +import { isButtonElement, isElementEmpty, isValidAriaPressed } from "./helpers/dom"; import { getExpectedAndReceivedStyles } from "./helpers/styles"; export class ElementAssertion extends Assertion { @@ -355,6 +355,75 @@ export class ElementAssertion extends Assertion { }); } + /** + * Asserts that the element is a pressed button. + * + * @returns the assertion instance. + */ + + public toBePressed(): this { + if (!isButtonElement(this.actual) || !isValidAriaPressed(this.actual)) { + throw new Error( + '.toBePressed() requires a button, input[type="button"], or role="button" with valid aria-pressed', + ); + } + + const pressedAttribute = this.actual.getAttribute("aria-pressed"); + const isPressed = pressedAttribute === "true"; + + const error = new AssertionError({ + actual: pressedAttribute, + expected: "true", + message: `Expected the element to be pressed, but received aria-pressed="${pressedAttribute}"`, + }); + + const invertedError = new AssertionError({ + actual: pressedAttribute, + expected: "false", + message: `Expected the element to NOT be pressed, but received aria-pressed="${pressedAttribute}"`, + }); + + return this.execute({ + assertWhen: isPressed, + error, + invertedError, + }); + } + + /** + * Asserts that the element is a partially pressed button. + * + * @returns the assertion instance. + */ + + public toBePartiallyPressed(): this { + if (!isButtonElement(this.actual) || !isValidAriaPressed(this.actual)) { + throw new Error( + '.toBePartiallyPressed() requires a button, input[type="button"], or role="button" with valid aria-pressed', + ); + } + + const pressedAttribute = this.actual.getAttribute("aria-pressed"); + const isPartiallyPressed = pressedAttribute === "mixed"; + + const error = new AssertionError({ + actual: pressedAttribute, + expected: "mixed", + message: `Expected the element to be partially pressed, but received aria-pressed="${pressedAttribute}"`, + }); + + const invertedError = new AssertionError({ + actual: pressedAttribute, + message: `Expected the element to NOT be partially pressed, but received aria-pressed="${pressedAttribute}"`, + }); + + return this.execute({ + assertWhen: isPartiallyPressed, + error, + invertedError, + }); + } + /** * Helper method to assert the presence or absence of class names. * diff --git a/packages/dom/src/lib/helpers/dom.ts b/packages/dom/src/lib/helpers/dom.ts index 62d0012..a52bd0a 100644 --- a/packages/dom/src/lib/helpers/dom.ts +++ b/packages/dom/src/lib/helpers/dom.ts @@ -4,3 +4,23 @@ export function isElementEmpty(element: Element): boolean { const nonCommentChildNodes = [...element.childNodes].filter(child => child.nodeType !== COMMENT_NODE_TYPE); return nonCommentChildNodes.length === 0; } + +export function isButtonElement(element: Element): boolean { + const roles = (element.getAttribute("role") || "") + .split(" ") + .map(role => role.trim()); + + const tagName = element.tagName.toLowerCase(); + const type = element.getAttribute("type"); + + return ( + tagName === "button" + || (tagName === "input" && type === "button") + || roles.includes("button") + ); +} + +export function isValidAriaPressed(element: Element): boolean { + const pressedAttribute = element.getAttribute("aria-pressed"); + return pressedAttribute === "true" || pressedAttribute === "false" || pressedAttribute === "mixed"; +} diff --git a/packages/dom/test/unit/lib/ElementAssertion.test.tsx b/packages/dom/test/unit/lib/ElementAssertion.test.tsx index f6f4093..ec746b2 100644 --- a/packages/dom/test/unit/lib/ElementAssertion.test.tsx +++ b/packages/dom/test/unit/lib/ElementAssertion.test.tsx @@ -5,6 +5,7 @@ import { ElementAssertion } from "../../../src/lib/ElementAssertion"; import { HaveClassTest } from "./fixtures/HaveClassTest"; import { NestedElementsTest } from "./fixtures/NestedElementsTest"; +import { PressedTestComponent } from "./fixtures/PressedTestComponent"; import { SimpleTest } from "./fixtures/SimpleTest"; import { WithAttributesTest } from "./fixtures/WithAttributesTest"; import { DescriptionTestComponent } from "./fixtures/descriptionTestComponent"; @@ -586,4 +587,240 @@ describe("[Unit] ElementAssertion.test.ts", () => { }); }); }); + + describe(".toBePressed", () => { + context("when the element is a valid button-like element", () => { + context("when aria-pressed is \"true\"", () => { + it("returns the assertion instance", () => { + const { getByTestId } = render(); + const button = getByTestId("button-pressed"); + const test = new ElementAssertion(button); + + expect(test.toBePressed()).toBeEqual(test); + + expect(() => test.not.toBePressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to NOT be pressed, but received aria-pressed="true"'); + }); + }); + + context("when aria-pressed is \"false\"", () => { + it("throws an assertion error", () => { + const { getByTestId } = render(); + const button = getByTestId("button-not-pressed"); + const test = new ElementAssertion(button); + + expect(() => test.toBePressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to be pressed, but received aria-pressed="false"'); + + expect(test.not.toBePressed()).toBeEqual(test); + }); + }); + + context("when aria-pressed is \"mixed\"", () => { + it("throws an assertion error", () => { + const { getByTestId } = render(); + const button = getByTestId("button-mixed"); + const test = new ElementAssertion(button); + + expect(() => test.toBePressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to be pressed, but received aria-pressed="mixed"'); + + expect(test.not.toBePressed()).toBeEqual(test); + }); + }); + + context("when the element is an input with type=\"button\"", () => { + it("returns the assertion instance when aria-pressed is \"true\"", () => { + const { getByTestId } = render(); + const input = getByTestId("input-button-pressed"); + const test = new ElementAssertion(input); + + expect(test.toBePressed()).toBeEqual(test); + + expect(() => test.not.toBePressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to NOT be pressed, but received aria-pressed="true"'); + }); + + it("throws an assertion error when aria-pressed is \"false\"", () => { + const { getByTestId } = render(); + const input = getByTestId("input-button-not-pressed"); + const test = new ElementAssertion(input); + + expect(() => test.toBePressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to be pressed, but received aria-pressed="false"'); + + expect(test.not.toBePressed()).toBeEqual(test); + }); + }); + + context("when the element has role=\"button\"", () => { + it("returns the assertion instance when aria-pressed is \"true\"", () => { + const { getByTestId } = render(); + const div = getByTestId("role-button-pressed"); + const test = new ElementAssertion(div); + + expect(test.toBePressed()).toBeEqual(test); + + expect(() => test.not.toBePressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to NOT be pressed, but received aria-pressed="true"'); + }); + + it("throws an assertion error when aria-pressed is \"false\"", () => { + const { getByTestId } = render(); + const div = getByTestId("role-button-not-pressed"); + const test = new ElementAssertion(div); + + expect(() => test.toBePressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to be pressed, but received aria-pressed="false"'); + + expect(test.not.toBePressed()).toBeEqual(test); + }); + }); + }); + + context("when the element is not a valid button-like element", () => { + it("throws a plain Error", () => { + const { getByTestId } = render(); + const div = getByTestId("non-button-element"); + const test = new ElementAssertion(div); + + expect(() => test.toBePressed()).toThrowError(Error); + }); + }); + + context("when aria-pressed is missing", () => { + it("throws a plain Error", () => { + const { getByTestId } = render(); + const button = getByTestId("button-no-aria-pressed"); + const test = new ElementAssertion(button); + + expect(() => test.toBePressed()).toThrowError(Error); + }); + }); + }); + + describe(".toBePartiallyPressed", () => { + context("when the element is a valid button-like element", () => { + context("when aria-pressed is \"mixed\"", () => { + it("returns the assertion instance", () => { + const { getByTestId } = render(); + const button = getByTestId("button-mixed"); + const test = new ElementAssertion(button); + + expect(test.toBePartiallyPressed()).toBeEqual(test); + + expect(() => test.not.toBePartiallyPressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to NOT be partially pressed, but received aria-pressed="mixed"'); + }); + }); + + context("when aria-pressed is \"true\"", () => { + it("throws an assertion error", () => { + const { getByTestId } = render(); + const button = getByTestId("button-pressed"); + const test = new ElementAssertion(button); + + expect(() => test.toBePartiallyPressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to be partially pressed, but received aria-pressed="true"'); + + expect(test.not.toBePartiallyPressed()).toBeEqual(test); + }); + }); + + context("when aria-pressed is \"false\"", () => { + it("throws an assertion error", () => { + const { getByTestId } = render(); + const button = getByTestId("button-not-pressed"); + const test = new ElementAssertion(button); + + expect(() => test.toBePartiallyPressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to be partially pressed, but received aria-pressed="false"'); + + expect(test.not.toBePartiallyPressed()).toBeEqual(test); + }); + }); + + context("when the element is an input with type=\"button\"", () => { + it("returns the assertion instance when aria-pressed is \"mixed\"", () => { + const { getByTestId } = render(); + const input = getByTestId("input-button-mixed"); + const test = new ElementAssertion(input); + + expect(test.toBePartiallyPressed()).toBeEqual(test); + + expect(() => test.not.toBePartiallyPressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to NOT be partially pressed, but received aria-pressed="mixed"'); + }); + + it("throws an assertion error when aria-pressed is \"false\"", () => { + const { getByTestId } = render(); + const input = getByTestId("input-button-not-pressed"); + const test = new ElementAssertion(input); + + expect(() => test.toBePartiallyPressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to be partially pressed, but received aria-pressed="false"'); + + expect(test.not.toBePartiallyPressed()).toBeEqual(test); + }); + }); + + context("when the element has role=\"button\"", () => { + it("returns the assertion instance when aria-pressed is \"mixed\"", () => { + const { getByTestId } = render(); + const div = getByTestId("role-button-mixed"); + const test = new ElementAssertion(div); + + expect(test.toBePartiallyPressed()).toBeEqual(test); + + expect(() => test.not.toBePartiallyPressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to NOT be partially pressed, but received aria-pressed="mixed"'); + }); + + it("throws an assertion error when aria-pressed is \"false\"", () => { + const { getByTestId } = render(); + const div = getByTestId("role-button-not-pressed"); + const test = new ElementAssertion(div); + + expect(() => test.toBePartiallyPressed()) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to be partially pressed, but received aria-pressed="false"'); + + expect(test.not.toBePartiallyPressed()).toBeEqual(test); + }); + }); + }); + + context("when the element is not a valid button-like element", () => { + it("throws a plain Error", () => { + const { getByTestId } = render(); + const div = getByTestId("non-button-element"); + const test = new ElementAssertion(div); + + expect(() => test.toBePartiallyPressed()).toThrowError(Error); + }); + }); + + context("when aria-pressed is missing", () => { + it("throws a plain Error", () => { + const { getByTestId } = render(); + const button = getByTestId("button-no-aria-pressed"); + const test = new ElementAssertion(button); + + expect(() => test.toBePartiallyPressed()).toThrowError(Error); + }); + }); + }); }); diff --git a/packages/dom/test/unit/lib/fixtures/PressedTestComponent.tsx b/packages/dom/test/unit/lib/fixtures/PressedTestComponent.tsx new file mode 100644 index 0000000..e0f94c5 --- /dev/null +++ b/packages/dom/test/unit/lib/fixtures/PressedTestComponent.tsx @@ -0,0 +1,26 @@ +import type { ReactElement } from "react"; + +export function PressedTestComponent(): ReactElement { + return ( +
+ {/* + + + + + {/* variants */} + + + + + {/* role="button" variants */} +
{"Pressed"}
+
{"Not pressed"}
+
{"Mixed"}
+ + {/* invalid element – no button role/tag */} +
{"Not a button"}
+
+ ); +}