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
54 changes: 53 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, isValidAriaPressed } from "./helpers/accessibility";
import { isButtonElement, isElementEmpty, normalizeHtml } from "./helpers/dom";
import { isButtonElement, isElementEmpty, isElementOrAncestorDisabled, normalizeHtml } from "./helpers/dom";
import { getExpectedAndReceivedStyles } from "./helpers/styles";

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

/**
* Asserts that the element is disabled.
*
* @example
* expect(element).toBeDisabled();
* expect(element).not.toBeDisabled();
*
* @returns the assertion instance.
*/
public toBeDisabled(): this {
const isDisabled = isElementOrAncestorDisabled(this.actual);
const error = new AssertionError({
actual: this.actual,
message: "Expected the element to be disabled",
});
const invertedError = new AssertionError({
actual: this.actual,
message: "Expected the element to NOT be disabled",
});
return this.execute({
assertWhen: isDisabled,
error,
invertedError,
});
}

/**
* Asserts that the element is enabled.
*
* @example
* expect(element).toBeEnabled();
* expect(element).not.toBeEnabled();
*
* @returns the assertion instance.
*/
public toBeEnabled(): this {
const isEnabled = !isElementOrAncestorDisabled(this.actual);
const error = new AssertionError({
actual: this.actual,
message: "Expected the element to be enabled",
});
const invertedError = new AssertionError({
actual: this.actual,
message: "Expected the element to NOT be enabled",
});
return this.execute({
assertWhen: isEnabled,
error,
invertedError,
});
}

/**
* Helper method to assert the presence or absence of class names.
*
Expand Down
53 changes: 53 additions & 0 deletions packages/dom/src/lib/helpers/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,56 @@ export function isButtonElement(element: Element): boolean {

return isNativeButton || hasButtonRole;
}

const FORM_TAGS = [
"fieldset",
"input",
"select",
"optgroup",
"option",
"button",
"textarea",
];

function isFirstLegendChildOfFieldset(element: Element, parent: Element | null): boolean {
return (
element.tagName.toLowerCase() === "legend"
&& parent?.tagName.toLowerCase() === "fieldset"
&& element === Array.from(parent.children).find(child => child.tagName.toLowerCase() === "legend")
);
}

function isElementDisabledByParent(element: Element, parent: Element | null): boolean {
return (
parent !== null
&& isElementDisabled(parent) && !isFirstLegendChildOfFieldset(element, parent)
);
}

function isCustomElement(tag: string): boolean {
return tag.includes("-");
}

function canElementBeDisabled(element: Element): boolean {
const tag = element.tagName.toLowerCase();
return FORM_TAGS.includes(tag) || isCustomElement(tag);
}

function isElementDisabled(element: Element): boolean {
return canElementBeDisabled(element) && element.hasAttribute("disabled");
}

function isAncestorDisabled(element: Element): boolean {
const parent = element.parentElement;
return (
parent !== null
&& (isElementDisabledByParent(element, parent) || isAncestorDisabled(parent))
);
}

export function isElementOrAncestorDisabled(element: Element): boolean {
return (
canElementBeDisabled(element)
&& (isElementDisabled(element) || isAncestorDisabled(element))
);
}
84 changes: 84 additions & 0 deletions packages/dom/test/unit/lib/ElementAssertion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { render } from "@testing-library/react";

import { ElementAssertion } from "../../../src/lib/ElementAssertion";

import { DisabledTestComponent } from "./fixtures/DisabledTestComponent";
import { HaveClassTest } from "./fixtures/HaveClassTest";
import { NestedElementsTest } from "./fixtures/NestedElementsTest";
import { PressedTestComponent } from "./fixtures/PressedTestComponent";
Expand Down Expand Up @@ -904,4 +905,87 @@ describe("[Unit] ElementAssertion.test.ts", () => {
});
});
});

describe(".toBeDisabled", () => {
context("when the element is disabled", () => {
it("returns the assertion instance", () => {
const { getByTestId } = render(<DisabledTestComponent />);

const buttonDisabled = getByTestId("button-disabled");
expect(new ElementAssertion(buttonDisabled).toBeDisabled()).toBeInstanceOf(ElementAssertion);

const inputDisabled = getByTestId("input-disabled");
expect(new ElementAssertion(inputDisabled).toBeDisabled()).toBeInstanceOf(ElementAssertion);

const customDisabled = getByTestId("custom-disabled");
expect(new ElementAssertion(customDisabled).toBeDisabled()).toBeInstanceOf(ElementAssertion);

const fieldsetChildDisabled = getByTestId("fieldset-child-disabled");
expect(new ElementAssertion(fieldsetChildDisabled).toBeDisabled()).toBeInstanceOf(ElementAssertion);

const fieldsetLegendSecondChildDisabled = getByTestId("fieldset-legend-second-child-disabled");
expect(new ElementAssertion(fieldsetLegendSecondChildDisabled).toBeDisabled()).toBeInstanceOf(ElementAssertion);

const fieldsetNestedChildDisabled = getByTestId("fieldset-nested-child-disabled");
expect(new ElementAssertion(fieldsetNestedChildDisabled).toBeDisabled()).toBeInstanceOf(ElementAssertion);
});
});

context("when the element is not disabled", () => {
it("throws an assertion error", () => {
const { getByTestId } = render(<DisabledTestComponent />);

const buttonEnabled = getByTestId("button-enabled");
const test = new ElementAssertion(buttonEnabled);

expect(() => test.toBeDisabled())
.toThrowError(AssertionError)
.toHaveMessage("Expected the element to be disabled");
expect(test.not.toBeDisabled()).toBeEqual(test);

const divDisabled = getByTestId("div-disabled");
const testDiv = new ElementAssertion(divDisabled);
expect(() => testDiv.toBeDisabled())
.toThrowError(AssertionError)
.toHaveMessage("Expected the element to be disabled");

const fieldsetLegendFirstChild = getByTestId("fieldset-legend-first-child");
const testLegendFirstChild = new ElementAssertion(fieldsetLegendFirstChild);
expect(() => testLegendFirstChild.toBeDisabled())
.toThrowError(AssertionError)
.toHaveMessage("Expected the element to be disabled");
});
});
});

describe(".toBeEnabled", () => {
context("when the element is enabled", () => {
it("returns the assertion instance", () => {
const { getByTestId } = render(<DisabledTestComponent />);

const buttonEnabled = getByTestId("button-enabled");
expect(new ElementAssertion(buttonEnabled).toBeEnabled()).toBeInstanceOf(ElementAssertion);

const divDisabled = getByTestId("div-disabled");
expect(new ElementAssertion(divDisabled).toBeEnabled()).toBeInstanceOf(ElementAssertion);

const fieldsetLegendFirstChild = getByTestId("fieldset-legend-first-child");
expect(new ElementAssertion(fieldsetLegendFirstChild).toBeEnabled()).toBeInstanceOf(ElementAssertion);
});
});

context("when the element is not enabled", () => {
it("throws an assertion error", () => {
const { getByTestId } = render(<DisabledTestComponent />);

const buttonDisabled = getByTestId("button-disabled");
const test = new ElementAssertion(buttonDisabled);

expect(() => test.toBeEnabled())
.toThrowError(AssertionError)
.toHaveMessage("Expected the element to be enabled");
expect(test.not.toBeEnabled()).toBeEqual(test);
});
});
});
});
29 changes: 29 additions & 0 deletions packages/dom/test/unit/lib/fixtures/DisabledTestComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ReactElement } from "react";

export function DisabledTestComponent(): ReactElement {
return (
<div>
<button data-testid="button-disabled" disabled={true}>{"Disabled"}</button>
<button data-testid="button-enabled">{"Enabled"}</button>
<input data-testid="input-disabled" disabled={true} />

<div data-testid="div-disabled" {...{ disabled: true }}>{"Div Disabled"}</div>

{/* @ts-expect-error - Custom element */}
<custom-element data-testid="custom-disabled" disabled={true} />

<fieldset data-testid="fieldset-disabled" disabled={true}>
<button data-testid="fieldset-child-disabled">{"Disabled child"}</button>
<legend>
<button data-testid="fieldset-legend-first-child">{"Enabled child in first legend"}</button>
</legend>
<legend>
<button data-testid="fieldset-legend-second-child-disabled">{"Disabled child in second legend"}</button>
</legend>
<div>
<button data-testid="fieldset-nested-child-disabled">{"Nested disabled child"}</button>
</div>
</fieldset>
</div>
);
}
Loading