From 00eb2af12edb1a53f378ed08ef9739265296f447 Mon Sep 17 00:00:00 2001 From: Edwin Hernandez Date: Thu, 3 Oct 2024 16:27:59 -0500 Subject: [PATCH 1/5] Added initial logic structure --- packages/dom/src/lib/ElementAssertion.ts | 32 +++++++++++++ .../test/unit/lib/ElementAssertion.test.tsx | 48 +++++++++++++++++++ .../lib/fixtures/haveClassTestComponent.tsx | 9 ++++ 3 files changed, 89 insertions(+) create mode 100644 packages/dom/test/unit/lib/fixtures/haveClassTestComponent.tsx diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index ace918f..b7ee55d 100644 --- a/packages/dom/src/lib/ElementAssertion.ts +++ b/packages/dom/src/lib/ElementAssertion.ts @@ -90,4 +90,36 @@ export class ElementAssertion extends Assertion { invertedError, }); } + + public toHaveClass(classNames: string | string[], options: { exact?: boolean } = {}): this { + 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, + }); + } } diff --git a/packages/dom/test/unit/lib/ElementAssertion.test.tsx b/packages/dom/test/unit/lib/ElementAssertion.test.tsx index 2d231e0..e74ef37 100644 --- a/packages/dom/test/unit/lib/ElementAssertion.test.tsx +++ b/packages/dom/test/unit/lib/ElementAssertion.test.tsx @@ -6,6 +6,7 @@ import { ElementAssertion } from "../../../src/lib/ElementAssertion"; import { NestedElementsTestComponent } from "./fixtures/nestedElementsTestComponent"; import { SimpleTestComponent } from "./fixtures/simpleTestComponent"; import { WithAttributesTestComponent } from "./fixtures/withAttributesTestComponent"; +import { HaveClassTestComponent } from "./fixtures/haveClassTestComponent"; describe("[Unit] ElementAssertion.test.ts", () => { describe(".toBeInTheDocument", () => { @@ -172,4 +173,51 @@ 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(); + const divTest = await findByTestId("classTest"); + divTest.className = "foo bar"; + const test = new ElementAssertion(divTest); + expect(test.toHaveClass("foo")).toBeEqual(test); + }); + }); + + 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.className = "foo"; + const test = new ElementAssertion(divTest); + expect(() => test.toHaveClass("bar")) + .toThrowError(AssertionError) + .toHaveMessage(`Expected the element to have class(es): "bar"`); + }); + }); + + context("when the element element has the the exact matching expected class", () => { + it("returns the assertion instance", async () => { + const { findByTestId } = render(); + const divTest = await findByTestId("classTest"); + divTest.className = "foo bar"; + const test = new ElementAssertion(divTest); + expect(test.toHaveClass(["foo", "bar"], { exact: true })).toBeEqual(test); + }); + }); + + context("when the element does not have the exact matching expected class ", async () => { + it("throws an assertion error", async () => { + const { findByTestId } = render(); + 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"`); + }); + }); + }); + }); 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..f9331c3 --- /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 +
+ ); +} From b5fddc5cba84534b0cd01be26aa49c621d1fd8b6 Mon Sep 17 00:00:00 2001 From: Edwin Hernandez Date: Thu, 3 Oct 2024 16:42:19 -0500 Subject: [PATCH 2/5] Fix linting errors --- packages/dom/src/lib/ElementAssertion.ts | 13 +++++++------ .../dom/test/unit/lib/ElementAssertion.test.tsx | 8 ++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index b7ee55d..d5049f1 100644 --- a/packages/dom/src/lib/ElementAssertion.ts +++ b/packages/dom/src/lib/ElementAssertion.ts @@ -91,7 +91,7 @@ export class ElementAssertion extends Assertion { }); } - public toHaveClass(classNames: string | string[], options: { exact?: boolean } = {}): this { + public toHaveClass(classNames: string | string[], options: { exact?: boolean; } = {}): this { const actualClassList = this.actual.className.split(/\s+/).filter(Boolean); const expectedClassList = Array.isArray(classNames) ? classNames : [classNames]; const { exact = false } = options; @@ -100,20 +100,21 @@ export class ElementAssertion extends Assertion { 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(' ')}"`, + ? `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(' ')}"`, + ? `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)) + ? actualClassList.length === expectedClassList.length + && expectedClassList.every(cls => actualClassList.includes(cls)) : expectedClassList.every(cls => actualClassList.includes(cls)); return this.execute({ diff --git a/packages/dom/test/unit/lib/ElementAssertion.test.tsx b/packages/dom/test/unit/lib/ElementAssertion.test.tsx index e74ef37..3129989 100644 --- a/packages/dom/test/unit/lib/ElementAssertion.test.tsx +++ b/packages/dom/test/unit/lib/ElementAssertion.test.tsx @@ -3,10 +3,10 @@ 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"; -import { HaveClassTestComponent } from "./fixtures/haveClassTestComponent"; describe("[Unit] ElementAssertion.test.ts", () => { describe(".toBeInTheDocument", () => { @@ -193,7 +193,7 @@ describe("[Unit] ElementAssertion.test.ts", () => { const test = new ElementAssertion(divTest); expect(() => test.toHaveClass("bar")) .toThrowError(AssertionError) - .toHaveMessage(`Expected the element to have class(es): "bar"`); + .toHaveMessage("Expected the element to have class(es): \"bar\""); }); }); @@ -207,7 +207,7 @@ describe("[Unit] ElementAssertion.test.ts", () => { }); }); - context("when the element does not have the exact matching expected class ", async () => { + context("when the element does not have the exact matching expected class ", () => { it("throws an assertion error", async () => { const { findByTestId } = render(); const divTest = await findByTestId("classTest"); @@ -215,7 +215,7 @@ describe("[Unit] ElementAssertion.test.ts", () => { 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"`); + .toHaveMessage("Expected the element to have exactly these classes: \"foo bar\""); }); }); }); From e3973e832e51889b654cc38ad83f3989c09a654a Mon Sep 17 00:00:00 2001 From: Edwin Hernandez Date: Fri, 4 Oct 2024 15:10:24 -0500 Subject: [PATCH 3/5] Add inverted errors and tests --- .../dom/test/unit/lib/ElementAssertion.test.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/dom/test/unit/lib/ElementAssertion.test.tsx b/packages/dom/test/unit/lib/ElementAssertion.test.tsx index 3129989..85e7779 100644 --- a/packages/dom/test/unit/lib/ElementAssertion.test.tsx +++ b/packages/dom/test/unit/lib/ElementAssertion.test.tsx @@ -181,7 +181,12 @@ describe("[Unit] ElementAssertion.test.ts", () => { const divTest = await findByTestId("classTest"); divTest.className = "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(es): \"foo\""); }); }); @@ -191,9 +196,12 @@ describe("[Unit] ElementAssertion.test.ts", () => { 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); }); }); @@ -203,7 +211,12 @@ describe("[Unit] ElementAssertion.test.ts", () => { 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\""); }); }); @@ -213,9 +226,12 @@ describe("[Unit] ElementAssertion.test.ts", () => { 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); }); }); }); From 357dab8f74461495150e482c3acf37818eae89ab Mon Sep 17 00:00:00 2001 From: Edwin Hernandez Date: Fri, 4 Oct 2024 15:42:11 -0500 Subject: [PATCH 4/5] Add anotations --- packages/dom/src/lib/ElementAssertion.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index d5049f1..9f94fe2 100644 --- a/packages/dom/src/lib/ElementAssertion.ts +++ b/packages/dom/src/lib/ElementAssertion.ts @@ -91,6 +91,17 @@ export class ElementAssertion extends Assertion { }); } +/** + * 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 { const actualClassList = this.actual.className.split(/\s+/).filter(Boolean); const expectedClassList = Array.isArray(classNames) ? classNames : [classNames]; From a831c903dda0dee24fc0a7d244ca9bdc281a4c0f Mon Sep 17 00:00:00 2001 From: Edwin Hernandez Date: Thu, 3 Apr 2025 12:24:05 -0500 Subject: [PATCH 5/5] Add new matches based on the class input --- packages/dom/src/lib/ElementAssertion.ts | 105 +++++++++++++----- .../test/unit/lib/ElementAssertion.test.tsx | 68 +++++++++--- .../lib/fixtures/haveClassTestComponent.tsx | 2 +- 3 files changed, 128 insertions(+), 47 deletions(-) diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index 9f94fe2..14e8afa 100644 --- a/packages/dom/src/lib/ElementAssertion.ts +++ b/packages/dom/src/lib/ElementAssertion.ts @@ -91,45 +91,92 @@ export class ElementAssertion extends Assertion { }); } -/** - * 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 { - const actualClassList = this.actual.className.split(/\s+/).filter(Boolean); - const expectedClassList = Array.isArray(classNames) ? classNames : [classNames]; - const { exact = false } = options; + /** + * 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: expectedClassList, - message: exact - ? `Expected the element to have exactly these classes: "${expectedClassList.join(" ")}"` - : `Expected the element to have class(es): "${expectedClassList.join(" ")}"`, + expected: classNames, + message, }); 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(" ")}"`, + expected: classNames, + message: invertedMessage, }); - const assertWhen = exact - ? actualClassList.length === expectedClassList.length - && expectedClassList.every(cls => actualClassList.includes(cls)) - : expectedClassList.every(cls => actualClassList.includes(cls)); - return this.execute({ - assertWhen, + 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 85e7779..ec00f0d 100644 --- a/packages/dom/test/unit/lib/ElementAssertion.test.tsx +++ b/packages/dom/test/unit/lib/ElementAssertion.test.tsx @@ -175,63 +175,97 @@ describe("[Unit] ElementAssertion.test.ts", () => { }); describe(".toHaveClass", () => { - context("when the element has the the expected class", () => { + context("when the element has the expected class", () => { it("returns the assertion instance", async () => { const { findByTestId } = render(); const divTest = await findByTestId("classTest"); - divTest.className = "foo bar"; + 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(es): \"foo\""); + .toHaveMessage('Expected the element to NOT have class: "foo"'); }); }); - context("when the element does not have the expected class ", () => { + 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.toHaveClass("bar")) + expect(() => test.toHaveAnyClass("bar", "baz")) .toThrowError(AssertionError) - .toHaveMessage("Expected the element to have class(es): \"bar\""); + .toHaveMessage('Expected the element to have at least one of these classes: "bar baz"'); - expect(test.not.toHaveClass("bar")).toBeEqual(test); + expect(test.not.toHaveAnyClass("bar", "baz")).toBeEqual(test); }); }); + }); - context("when the element element has the the exact matching expected class", () => { + 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.className = "foo bar"; + divTest.classList.add("foo", "bar", "baz"); const test = new ElementAssertion(divTest); - expect(test.toHaveClass(["foo", "bar"], { exact: true })).toBeEqual(test); + expect(test.toHaveAllClasses("foo", "bar")).toBeEqual(test); - expect(() => test.not.toHaveClass(["foo", "bar"], { exact: true })) + expect(() => test.not.toHaveAllClasses("foo", "bar")) .toThrowError(AssertionError) - .toHaveMessage("Expected the element to NOT have exactly these classes: \"foo bar\""); + .toHaveMessage('Expected the element to NOT have all of these classes: "foo bar"'); }); }); - context("when the element does not have the exact matching expected class ", () => { + 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.className = "foo bar extra"; + divTest.classList.add("foo", "bar"); const test = new ElementAssertion(divTest); - expect(() => test.toHaveClass(["foo", "bar"], { exact: true })) + expect(() => test.toHaveAllClasses("foo", "bar", "baz")) .toThrowError(AssertionError) - .toHaveMessage("Expected the element to have exactly these classes: \"foo bar\""); + .toHaveMessage('Expected the element to have all of these classes: "foo bar baz"'); - expect(test.not.toHaveClass(["foo", "bar"], { exact: true })).toBeEqual(test); + 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 index f9331c3..a03ad0a 100644 --- a/packages/dom/test/unit/lib/fixtures/haveClassTestComponent.tsx +++ b/packages/dom/test/unit/lib/fixtures/haveClassTestComponent.tsx @@ -3,7 +3,7 @@ import { ReactElement } from "react"; export function HaveClassTestComponent(): ReactElement { return (
- Test text inside a div + {"Test text inside a div"}
); }