Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dom): DOM - toHaveClass #134

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
44 changes: 44 additions & 0 deletions packages/dom/src/lib/ElementAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,48 @@ export class ElementAssertion<T extends Element> extends Assertion<T> {
invertedError,
});
}

/**
* Check if the element has a specific class or classes.
*
* Validates that the provided element contains specified classes.
* Allows checking for one or more class names and supports exact matching.
*
* @param classNames - A single class name or an array of class names to check.
* @param options - Optional settings for matching:
* - `exact` (boolean): When true, checks for an exact match of all classes.
* @returns the assertion instance.
*/
public toHaveClass(classNames: string | string[], options: { exact?: boolean; } = {}): this {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about making more than one assertion instead of adding the options param? To me, it'll provide more readability if we have the following:

expect(elem).toHaveClass("w-full"); // single arg

expect(elem).toHaveAnyClass("w-full", "flex"); // variadic args

expect(elem).toHaveAllClasses("w-full", "flex", "gap-8"); // variadic args

const actualClassList = this.actual.className.split(/\s+/).filter(Boolean);
const expectedClassList = Array.isArray(classNames) ? classNames : [classNames];
const { exact = false } = options;

const error = new AssertionError({
actual: actualClassList,
expected: expectedClassList,
message: exact
? `Expected the element to have exactly these classes: "${expectedClassList.join(" ")}"`
: `Expected the element to have class(es): "${expectedClassList.join(" ")}"`,
});

const invertedError = new AssertionError({
actual: actualClassList,
expected: expectedClassList,
message: exact
? `Expected the element to NOT have exactly these classes: "${expectedClassList.join(" ")}"`
: `Expected the element to NOT have class(es): "${expectedClassList.join(" ")}"`,
});

const assertWhen = exact
? actualClassList.length === expectedClassList.length
&& expectedClassList.every(cls => actualClassList.includes(cls))
: expectedClassList.every(cls => actualClassList.includes(cls));

return this.execute({
assertWhen,
error,
invertedError,
});
}
}
70 changes: 67 additions & 3 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 { HaveClassTestComponent } from "./fixtures/haveClassTestComponent";
import { NestedElementsTestComponent } from "./fixtures/nestedElementsTestComponent";
import { SimpleTestComponent } from "./fixtures/simpleTestComponent";
import { WithAttributesTestComponent } from "./fixtures/withAttributesTestComponent";
Expand Down Expand Up @@ -69,7 +70,7 @@ describe("[Unit] ElementAssertion.test.ts", () => {

context("and it is an indirect child", () => {
it("returns the assertion instance", async () => {
const { findByTestId } = render(<NestedElementsTestComponent/>);
const { findByTestId } = render(<NestedElementsTestComponent />);
const grandparent = await findByTestId("grandparent");
const child = await findByTestId("child");
const grandparentTest = new ElementAssertion(grandparent);
Expand All @@ -84,7 +85,7 @@ describe("[Unit] ElementAssertion.test.ts", () => {

context("and it is a deeply nested child", () => {
it("returns the assertion instance", async () => {
const { findByTestId } = render(<NestedElementsTestComponent/>);
const { findByTestId } = render(<NestedElementsTestComponent />);
const grandparent = await findByTestId("grandparent");
const deepChild = await findByTestId("deep-child");
const grandparentTest = new ElementAssertion(grandparent);
Expand All @@ -101,7 +102,7 @@ describe("[Unit] ElementAssertion.test.ts", () => {
context("when element is NOT contained in ancestor element", () => {
it("throws an assertion error", async () => {
const notChildElement = document.createElement("span");
const { findByTestId } = render(<NestedElementsTestComponent/>);
const { findByTestId } = render(<NestedElementsTestComponent />);
const grandparent = await findByTestId("grandparent");
const grandparentTest = new ElementAssertion(grandparent);

Expand Down Expand Up @@ -172,4 +173,67 @@ describe("[Unit] ElementAssertion.test.ts", () => {
});
});
});

describe(".toHaveClass", () => {
context("when the element has the the expected class", () => {
it("returns the assertion instance", async () => {
const { findByTestId } = render(<HaveClassTestComponent />);
const divTest = await findByTestId("classTest");
divTest.className = "foo bar";
Copy link
Member

@JoseLion JoseLion Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use divTest.classList.add("foo", "bar") instead? 🤔

const test = new ElementAssertion(divTest);

expect(test.toHaveClass("foo")).toBeEqual(test);

expect(() => test.not.toHaveClass("foo"))
.toThrowError(AssertionError)
.toHaveMessage("Expected the element to NOT have class(es): \"foo\"");
});
});

context("when the element does not have the expected class ", () => {
it("throws an assertion error", async () => {
const { findByTestId } = render(<HaveClassTestComponent />);
const divTest = await findByTestId("classTest");
divTest.className = "foo";
const test = new ElementAssertion(divTest);

expect(() => test.toHaveClass("bar"))
.toThrowError(AssertionError)
.toHaveMessage("Expected the element to have class(es): \"bar\"");

expect(test.not.toHaveClass("bar")).toBeEqual(test);
});
});

context("when the element element has the the exact matching expected class", () => {
it("returns the assertion instance", async () => {
const { findByTestId } = render(<HaveClassTestComponent />);
const divTest = await findByTestId("classTest");
divTest.className = "foo bar";
const test = new ElementAssertion(divTest);

expect(test.toHaveClass(["foo", "bar"], { exact: true })).toBeEqual(test);

expect(() => test.not.toHaveClass(["foo", "bar"], { exact: true }))
.toThrowError(AssertionError)
.toHaveMessage("Expected the element to NOT have exactly these classes: \"foo bar\"");
});
});

context("when the element does not have the exact matching expected class ", () => {
it("throws an assertion error", async () => {
const { findByTestId } = render(<HaveClassTestComponent />);
const divTest = await findByTestId("classTest");
divTest.className = "foo bar extra";
const test = new ElementAssertion(divTest);

expect(() => test.toHaveClass(["foo", "bar"], { exact: true }))
.toThrowError(AssertionError)
.toHaveMessage("Expected the element to have exactly these classes: \"foo bar\"");

expect(test.not.toHaveClass(["foo", "bar"], { exact: true })).toBeEqual(test);
});
});
});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ReactElement } from "react";

export function HaveClassTestComponent(): ReactElement {
return (
<div data-testid="classTest">
Test text inside a div
</div>
);
}