Skip to content
85 changes: 83 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 } from "./helpers/accessibility";
import { isElementEmpty } from "./helpers/dom";
import { getAccessibleDescription, isValidAriaPressed } from "./helpers/accessibility";
import { isButtonElement, isElementEmpty } from "./helpers/dom";
import { getExpectedAndReceivedStyles } from "./helpers/styles";

export class ElementAssertion<T extends Element> extends Assertion<T> {
Expand Down Expand Up @@ -355,6 +355,87 @@ export class ElementAssertion<T extends Element> extends Assertion<T> {
});
}

/**
* Asserts that the element is a pressed button.
*
Comment thread
KeylaMunnoz marked this conversation as resolved.
* @example
* const toggleButton = document.querySelector('#toggle');
* expect(toggleButton).toBePressed(); // passes if aria-pressed="true"
* expect(toggleButton).not.toBePressed(); // fails if aria-pressed="true"
*
* @returns the assertion instance.
*/

public toBePressed(): this {
if (!isButtonElement(this.actual) || !isValidAriaPressed(this.actual)) {
throw new Error(
'Expected a button or button-like control with a valid pressed state: "true", "false", or "mixed".',
);
}

const pressedAttributeValue = this.actual.getAttribute("aria-pressed");
const isPressed = pressedAttributeValue === "true";

const error = new AssertionError({
actual: pressedAttributeValue,
expected: "true",
message: `Expected the element to be pressed, but received aria-pressed="${pressedAttributeValue}"`,
});

const invertedError = new AssertionError({
actual: pressedAttributeValue,
expected: "false",
message: `Expected the element to NOT be pressed, but received aria-pressed="${pressedAttributeValue}"`,
});

return this.execute({
assertWhen: isPressed,
error,
invertedError,
});
}

/**
* Asserts that the element is a partially pressed button.
*
Comment thread
KeylaMunnoz marked this conversation as resolved.
* @example
* const toggleButton = document.querySelector('#toggle');
* expect(toggleButton).toBePartiallyPressed();
* // passes if aria-pressed="mixed"
* expect(toggleButton).not.toBePartiallyPressed();
* // fails if aria-pressed="mixed"
*
* @returns the assertion instance.
*/

public toBePartiallyPressed(): this {
if (!isButtonElement(this.actual) || !isValidAriaPressed(this.actual)) {
throw new Error(
'Expected a button or button-like control with a valid pressed state: "true", "false", or "mixed".',
);
}

const pressedAttributeValue = this.actual.getAttribute("aria-pressed");
const isPartiallyPressed = pressedAttributeValue === "mixed";

const error = new AssertionError({
actual: pressedAttributeValue,
expected: "mixed",
message: `Expected the element to be partially pressed, but received aria-pressed="${pressedAttributeValue}"`,
});

const invertedError = new AssertionError({
actual: pressedAttributeValue,
message: `Expected the element to NOT be partially pressed, but received aria-pressed="${pressedAttributeValue}"`,
});

return this.execute({
assertWhen: isPartiallyPressed,
error,
invertedError,
});
}

/**
* Helper method to assert the presence or absence of class names.
*
Expand Down
5 changes: 5 additions & 0 deletions packages/dom/src/lib/helpers/accessibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ export function getAccessibleDescription(actual: Element): string {

return normalizeText(combinedText);
}

export function isValidAriaPressed(element: Element): boolean {
const pressedAttribute = element.getAttribute("aria-pressed");
return pressedAttribute !== null && ["true", "false", "mixed"].includes(pressedAttribute);
}
14 changes: 14 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,17 @@ export function isElementEmpty(element: Element): boolean {
const nonCommentChildNodes = [...element.childNodes].filter(child => child.nodeType !== COMMENT_NODE_TYPE);
return nonCommentChildNodes.length === 0;
}

Comment thread
KeylaMunnoz marked this conversation as resolved.
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");

const isNativeButton = tagName === "button" || (tagName === "input" && type === "button");
const hasButtonRole = roles.includes("button");

return isNativeButton || hasButtonRole;
}
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);
});
});
});
});
Loading
Loading