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
71 changes: 70 additions & 1 deletion packages/dom/src/lib/ElementAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends Element> extends Assertion<T> {
Expand Down Expand Up @@ -355,6 +355,75 @@ export class ElementAssertion<T extends Element> extends Assertion<T> {
});
}

/**
* 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.
*
Expand Down
20 changes: 20 additions & 0 deletions packages/dom/src/lib/helpers/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
237 changes: 237 additions & 0 deletions packages/dom/test/unit/lib/ElementAssertion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
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(<PressedTestComponent />);
const button = getByTestId("button-no-aria-pressed");
const test = new ElementAssertion(button);

expect(() => test.toBePartiallyPressed()).toThrowError(Error);
});
});
});
});
26 changes: 26 additions & 0 deletions packages/dom/test/unit/lib/fixtures/PressedTestComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ReactElement } from "react";

export function PressedTestComponent(): ReactElement {
return (
<div>
{/* <button> variants */}
<button data-testid="button-pressed" aria-pressed="true">{"Pressed"}</button>
<button data-testid="button-not-pressed" aria-pressed="false">{"Not pressed"}</button>
<button data-testid="button-mixed" aria-pressed="mixed">{"Mixed"}</button>
<button data-testid="button-no-aria-pressed">{"No aria-pressed"}</button>

{/* <input type="button"> variants */}
<input data-testid="input-button-pressed" type="button" aria-pressed="true" />
<input data-testid="input-button-not-pressed" type="button" aria-pressed="false" />
<input data-testid="input-button-mixed" type="button" aria-pressed="mixed" />

{/* role="button" variants */}
<div data-testid="role-button-pressed" role="button" aria-pressed="true">{"Pressed"}</div>
<div data-testid="role-button-not-pressed" role="button" aria-pressed="false">{"Not pressed"}</div>
<div data-testid="role-button-mixed" role="button" aria-pressed="mixed">{"Mixed"}</div>

{/* invalid element – no button role/tag */}
<div data-testid="non-button-element" aria-pressed="true">{"Not a button"}</div>
</div>
);
}
Loading