From d4d40c53ddbee9872783d5857b7d49a75497f85d Mon Sep 17 00:00:00 2001 From: kmunoz Date: Mon, 11 May 2026 11:49:40 -0500 Subject: [PATCH 1/4] feat(dom): toBePressed & toBePartiallyPressed --- packages/dom/src/lib/ElementAssertion.ts | 71 +++++++++++++++++++++++- packages/dom/src/lib/helpers/dom.ts | 20 +++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index 974d984..9c6cb58 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( + 'Only button or input with type="button" or element with role="button" and a valid aria-pressed attribute can be used with .toBePressed()', + ); + } + + 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( + 'Only button or input with type="button" or element with role="button" and a valid aria-pressed attribute can be used with .toBePartiallyPressed()', + ); + } + + 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"; +} From 582a78209d08f94d9c3f5dc0a9e83ca487664b24 Mon Sep 17 00:00:00 2001 From: kmunoz Date: Mon, 11 May 2026 13:06:14 -0500 Subject: [PATCH 2/4] feat(dom): tests added for toBePressed and toBePartiallyPressed --- .../test/unit/lib/ElementAssertion.test.tsx | 237 ++++++++++++++++++ .../lib/fixtures/PressedTestComponent.tsx | 26 ++ 2 files changed, 263 insertions(+) create mode 100644 packages/dom/test/unit/lib/fixtures/PressedTestComponent.tsx diff --git a/packages/dom/test/unit/lib/ElementAssertion.test.tsx b/packages/dom/test/unit/lib/ElementAssertion.test.tsx index f6f4093..c8f28ac 100644 --- a/packages/dom/test/unit/lib/ElementAssertion.test.tsx +++ b/packages/dom/test/unit/lib/ElementAssertion.test.tsx @@ -9,6 +9,7 @@ import { SimpleTest } from "./fixtures/SimpleTest"; import { WithAttributesTest } from "./fixtures/WithAttributesTest"; import { DescriptionTestComponent } from "./fixtures/descriptionTestComponent"; import { FocusTestComponent } from "./fixtures/focusTestComponent"; +import { PressedTestComponent } from "./fixtures/PressedTestComponent"; describe("[Unit] ElementAssertion.test.ts", () => { describe(".toBeInTheDocument", () => { @@ -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..6c86328 --- /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
+
+ ); +} From 8234a48ac7824073590bfbfb8d4a4f575ab88865 Mon Sep 17 00:00:00 2001 From: kmunoz Date: Mon, 11 May 2026 13:41:39 -0500 Subject: [PATCH 3/4] fix(dom): solving eslint errors --- packages/dom/src/lib/ElementAssertion.ts | 4 ++-- .../dom/test/unit/lib/ElementAssertion.test.tsx | 2 +- .../unit/lib/fixtures/PressedTestComponent.tsx | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index 9c6cb58..6fd919c 100644 --- a/packages/dom/src/lib/ElementAssertion.ts +++ b/packages/dom/src/lib/ElementAssertion.ts @@ -364,7 +364,7 @@ export class ElementAssertion extends Assertion { public toBePressed(): this { if (!isButtonElement(this.actual) || !isValidAriaPressed(this.actual)) { throw new Error( - 'Only button or input with type="button" or element with role="button" and a valid aria-pressed attribute can be used with .toBePressed()', + 'Only buttons or inputs with type="button", or elements with role="button" and valid aria-pressed, work with .toBePressed()', ); } @@ -399,7 +399,7 @@ export class ElementAssertion extends Assertion { public toBePartiallyPressed(): this { if (!isButtonElement(this.actual) || !isValidAriaPressed(this.actual)) { throw new Error( - 'Only button or input with type="button" or element with role="button" and a valid aria-pressed attribute can be used with .toBePartiallyPressed()', + 'Only buttons or inputs with type="button", or elements with role="button" and valid aria-pressed, work with .toBePartiallyPressed()', ); } diff --git a/packages/dom/test/unit/lib/ElementAssertion.test.tsx b/packages/dom/test/unit/lib/ElementAssertion.test.tsx index c8f28ac..ec746b2 100644 --- a/packages/dom/test/unit/lib/ElementAssertion.test.tsx +++ b/packages/dom/test/unit/lib/ElementAssertion.test.tsx @@ -5,11 +5,11 @@ 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"; import { FocusTestComponent } from "./fixtures/focusTestComponent"; -import { PressedTestComponent } from "./fixtures/PressedTestComponent"; describe("[Unit] ElementAssertion.test.ts", () => { describe(".toBeInTheDocument", () => { diff --git a/packages/dom/test/unit/lib/fixtures/PressedTestComponent.tsx b/packages/dom/test/unit/lib/fixtures/PressedTestComponent.tsx index 6c86328..e0f94c5 100644 --- a/packages/dom/test/unit/lib/fixtures/PressedTestComponent.tsx +++ b/packages/dom/test/unit/lib/fixtures/PressedTestComponent.tsx @@ -4,10 +4,10 @@ export function PressedTestComponent(): ReactElement { return (
{/* - - - + + + + {/* variants */} @@ -15,12 +15,12 @@ export function PressedTestComponent(): ReactElement { {/* role="button" variants */} -
Pressed
-
Not pressed
-
Mixed
+
{"Pressed"}
+
{"Not pressed"}
+
{"Mixed"}
{/* invalid element – no button role/tag */} -
Not a button
+
{"Not a button"}
); } From 56226c6c3ad03150b354283ab069c4e62f8c5265 Mon Sep 17 00:00:00 2001 From: kmunoz Date: Mon, 11 May 2026 13:49:20 -0500 Subject: [PATCH 4/4] solving eslint errors --- packages/dom/src/lib/ElementAssertion.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index 6fd919c..bb6d9f9 100644 --- a/packages/dom/src/lib/ElementAssertion.ts +++ b/packages/dom/src/lib/ElementAssertion.ts @@ -364,7 +364,7 @@ export class ElementAssertion extends Assertion { public toBePressed(): this { if (!isButtonElement(this.actual) || !isValidAriaPressed(this.actual)) { throw new Error( - 'Only buttons or inputs with type="button", or elements with role="button" and valid aria-pressed, work with .toBePressed()', + '.toBePressed() requires a button, input[type="button"], or role="button" with valid aria-pressed', ); } @@ -399,7 +399,7 @@ export class ElementAssertion extends Assertion { public toBePartiallyPressed(): this { if (!isButtonElement(this.actual) || !isValidAriaPressed(this.actual)) { throw new Error( - 'Only buttons or inputs with type="button", or elements with role="button" and valid aria-pressed, work with .toBePartiallyPressed()', + '.toBePartiallyPressed() requires a button, input[type="button"], or role="button" with valid aria-pressed', ); }