diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index ace918f..14e8afa 100644 --- a/packages/dom/src/lib/ElementAssertion.ts +++ b/packages/dom/src/lib/ElementAssertion.ts @@ -90,4 +90,95 @@ export class ElementAssertion extends Assertion { invertedError, }); } + + /** + * Asserts that the element has the specified class. + * + * @param className - The class name to check. + * @returns the assertion instance. + */ + public toHaveClass(className: string): this { + const actualClassList = this.getClassList(); + + return this.assertClassPresence( + actualClassList.includes(className), + [className], + `Expected the element to have class: "${className}"`, + `Expected the element to NOT have class: "${className}"`, + ); + } + + /** + * Asserts that the element has at least one of the specified classes. + * + * @param classNames - A variadic list of class names to check. + * @returns the assertion instance. + */ + public toHaveAnyClass(...classNames: string[]): this { + const actualClassList = this.getClassList(); + + return this.assertClassPresence( + classNames.some(cls => actualClassList.includes(cls)), + classNames, + `Expected the element to have at least one of these classes: "${classNames.join(" ")}"`, + `Expected the element to NOT have any of these classes: "${classNames.join(" ")}"`, + ); + } + + /** + * Asserts that the element has all of the specified classes. + * + * @param classNames - A variadic list of class names to check. + * @returns the assertion instance. + */ + public toHaveAllClasses(...classNames: string[]): this { + const actualClassList = this.getClassList(); + + return this.assertClassPresence( + classNames.every(cls => actualClassList.includes(cls)), + classNames, + `Expected the element to have all of these classes: "${classNames.join(" ")}"`, + `Expected the element to NOT have all of these classes: "${classNames.join(" ")}"`, + ); + } + + private getClassList(): string[] { + return this.actual.className.split(/\s+/).filter(Boolean); + } + + /** + * Helper method to assert the presence or absence of class names. + * + * @param assertCondition - Boolean to determine assertion pass or fail. + * @param classNames - Array of class names involved in the assertion. + * @param message - Assertion error message. + * @param invertedMessage - Inverted assertion error message. + * @returns the assertion instance. + */ + private assertClassPresence( + assertCondition: boolean, + classNames: string[], + message: string, + invertedMessage: string, + ): this { + const actualClassList = this.getClassList(); + + const error = new AssertionError({ + actual: actualClassList, + expected: classNames, + message, + }); + + const invertedError = new AssertionError({ + actual: actualClassList, + expected: classNames, + message: invertedMessage, + }); + + return this.execute({ + assertWhen: assertCondition, + error, + invertedError, + }); + } } diff --git a/packages/dom/test/unit/lib/ElementAssertion.test.tsx b/packages/dom/test/unit/lib/ElementAssertion.test.tsx index 2d231e0..ec00f0d 100644 --- a/packages/dom/test/unit/lib/ElementAssertion.test.tsx +++ b/packages/dom/test/unit/lib/ElementAssertion.test.tsx @@ -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"; @@ -172,4 +173,101 @@ describe("[Unit] ElementAssertion.test.ts", () => { }); }); }); + + describe(".toHaveClass", () => { + context("when the element has the expected class", () => { + it("returns the assertion instance", async () => { + const { findByTestId } = render(); + const divTest = await findByTestId("classTest"); + divTest.classList.add("foo", "bar"); + 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: "foo"'); + }); + }); + + context("when the element does not have the expected class", () => { + it("throws an assertion error", async () => { + const { findByTestId } = render(); + const divTest = await findByTestId("classTest"); + divTest.classList.add("foo", "bar"); + const test = new ElementAssertion(divTest); + + expect(() => test.toHaveClass("baz")) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to have class: "baz"'); + + expect(test.not.toHaveClass("baz")).toBeEqual(test); + }); + }); + }); + + describe(".toHaveAnyClass", () => { + context("when the element has at least one of the expected classes", () => { + it("returns the assertion instance", async () => { + const { findByTestId } = render(); + const divTest = await findByTestId("classTest"); + divTest.classList.add("foo", "bar"); + const test = new ElementAssertion(divTest); + + expect(test.toHaveAnyClass("bar", "baz")).toBeEqual(test); + + expect(() => test.not.toHaveAnyClass("bar", "baz")) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to NOT have any of these classes: "bar baz"'); + }); + }); + + context("when the element does not have any of the expected classes", () => { + it("throws an assertion error", async () => { + const { findByTestId } = render(); + const divTest = await findByTestId("classTest"); + divTest.className = "foo"; + const test = new ElementAssertion(divTest); + + expect(() => test.toHaveAnyClass("bar", "baz")) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to have at least one of these classes: "bar baz"'); + + expect(test.not.toHaveAnyClass("bar", "baz")).toBeEqual(test); + }); + }); + }); + + describe(".toHaveAllClasses", () => { + context("when the element has all the expected classes", () => { + it("returns the assertion instance", async () => { + const { findByTestId } = render(); + const divTest = await findByTestId("classTest"); + divTest.classList.add("foo", "bar", "baz"); + const test = new ElementAssertion(divTest); + + expect(test.toHaveAllClasses("foo", "bar")).toBeEqual(test); + + expect(() => test.not.toHaveAllClasses("foo", "bar")) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to NOT have all of these classes: "foo bar"'); + }); + }); + + context("when the element does not have all the expected classes", () => { + it("throws an assertion error", async () => { + const { findByTestId } = render(); + const divTest = await findByTestId("classTest"); + divTest.classList.add("foo", "bar"); + const test = new ElementAssertion(divTest); + + expect(() => test.toHaveAllClasses("foo", "bar", "baz")) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element to have all of these classes: "foo bar baz"'); + + expect(test.not.toHaveAllClasses("foo", "bar", "baz")).toBeEqual(test); + }); + }); + }); + }); diff --git a/packages/dom/test/unit/lib/fixtures/haveClassTestComponent.tsx b/packages/dom/test/unit/lib/fixtures/haveClassTestComponent.tsx new file mode 100644 index 0000000..a03ad0a --- /dev/null +++ b/packages/dom/test/unit/lib/fixtures/haveClassTestComponent.tsx @@ -0,0 +1,9 @@ +import { ReactElement } from "react"; + +export function HaveClassTestComponent(): ReactElement { + return ( +
+ {"Test text inside a div"} +
+ ); +}