Skip to content
Merged
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
83 changes: 81 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,85 @@ 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
* // It takes into account aria-pressed attribute
* expect(element).toBePressed();
* expect(element).not.toBePressed();
*
* @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
* // It takes into account aria-pressed attribute
* expect(element).toBePartiallyPressed();
* expect(element).not.toBePartiallyPressed();
*
Comment thread
KeylaMunnoz marked this conversation as resolved.
* @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);
});
});
});
});
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