Skip to content

Commit bd8b2a7

Browse files
authored
feat(dom): Add toHaveClass , toHaveAllClasses, and toHaveAnyClass assertions (#134)
1 parent 8cbace8 commit bd8b2a7

File tree

3 files changed

+193
-0
lines changed

3 files changed

+193
-0
lines changed

packages/dom/src/lib/ElementAssertion.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,95 @@ export class ElementAssertion<T extends Element> extends Assertion<T> {
9090
invertedError,
9191
});
9292
}
93+
94+
/**
95+
* Asserts that the element has the specified class.
96+
*
97+
* @param className The class name to check.
98+
* @returns the assertion instance.
99+
*/
100+
public toHaveClass(className: string): this {
101+
const actualClassList = this.getClassList();
102+
103+
return this.assertClassPresence(
104+
actualClassList.includes(className),
105+
[className],
106+
`Expected the element to have class: "${className}"`,
107+
`Expected the element to NOT have class: "${className}"`,
108+
);
109+
}
110+
111+
/**
112+
* Asserts that the element has at least one of the specified classes.
113+
*
114+
* @param classNames - A variadic list of class names to check.
115+
* @returns the assertion instance.
116+
*/
117+
public toHaveAnyClass(...classNames: string[]): this {
118+
const actualClassList = this.getClassList();
119+
120+
return this.assertClassPresence(
121+
classNames.some(cls => actualClassList.includes(cls)),
122+
classNames,
123+
`Expected the element to have at least one of these classes: "${classNames.join(" ")}"`,
124+
`Expected the element to NOT have any of these classes: "${classNames.join(" ")}"`,
125+
);
126+
}
127+
128+
/**
129+
* Asserts that the element has all of the specified classes.
130+
*
131+
* @param classNames - A variadic list of class names to check.
132+
* @returns the assertion instance.
133+
*/
134+
public toHaveAllClasses(...classNames: string[]): this {
135+
const actualClassList = this.getClassList();
136+
137+
return this.assertClassPresence(
138+
classNames.every(cls => actualClassList.includes(cls)),
139+
classNames,
140+
`Expected the element to have all of these classes: "${classNames.join(" ")}"`,
141+
`Expected the element to NOT have all of these classes: "${classNames.join(" ")}"`,
142+
);
143+
}
144+
145+
private getClassList(): string[] {
146+
return this.actual.className.split(/\s+/).filter(Boolean);
147+
}
148+
149+
/**
150+
* Helper method to assert the presence or absence of class names.
151+
*
152+
* @param assertCondition - Boolean to determine assertion pass or fail.
153+
* @param classNames - Array of class names involved in the assertion.
154+
* @param message - Assertion error message.
155+
* @param invertedMessage - Inverted assertion error message.
156+
* @returns the assertion instance.
157+
*/
158+
private assertClassPresence(
159+
assertCondition: boolean,
160+
classNames: string[],
161+
message: string,
162+
invertedMessage: string,
163+
): this {
164+
const actualClassList = this.getClassList();
165+
166+
const error = new AssertionError({
167+
actual: actualClassList,
168+
expected: classNames,
169+
message,
170+
});
171+
172+
const invertedError = new AssertionError({
173+
actual: actualClassList,
174+
expected: classNames,
175+
message: invertedMessage,
176+
});
177+
178+
return this.execute({
179+
assertWhen: assertCondition,
180+
error,
181+
invertedError,
182+
});
183+
}
93184
}

packages/dom/test/unit/lib/ElementAssertion.test.tsx

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { render } from "@testing-library/react";
33

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

6+
import { HaveClassTestComponent } from "./fixtures/haveClassTestComponent";
67
import { NestedElementsTestComponent } from "./fixtures/nestedElementsTestComponent";
78
import { SimpleTestComponent } from "./fixtures/simpleTestComponent";
89
import { WithAttributesTestComponent } from "./fixtures/withAttributesTestComponent";
@@ -172,4 +173,96 @@ describe("[Unit] ElementAssertion.test.ts", () => {
172173
});
173174
});
174175
});
176+
177+
describe(".toHaveClass", () => {
178+
context("when the element has the expected class", () => {
179+
it("returns the assertion instance", () => {
180+
const { getByText } = render(<HaveClassTestComponent className="foo bar" />);
181+
const divTest = getByText("Test text inside a div");
182+
const test = new ElementAssertion(divTest);
183+
184+
expect(test.toHaveClass("foo")).toBeEqual(test);
185+
186+
expect(() => test.not.toHaveClass("foo"))
187+
.toThrowError(AssertionError)
188+
.toHaveMessage('Expected the element to NOT have class: "foo"');
189+
});
190+
});
191+
192+
context("when the element does not have the expected class", () => {
193+
it("throws an assertion error", () => {
194+
const { getByText } = render(<HaveClassTestComponent className="foo bar" />);
195+
const divTest = getByText("Test text inside a div");
196+
const test = new ElementAssertion(divTest);
197+
198+
expect(() => test.toHaveClass("baz"))
199+
.toThrowError(AssertionError)
200+
.toHaveMessage('Expected the element to have class: "baz"');
201+
202+
expect(test.not.toHaveClass("baz")).toBeEqual(test);
203+
});
204+
});
205+
});
206+
207+
describe(".toHaveAnyClass", () => {
208+
context("when the element has at least one of the expected classes", () => {
209+
it("returns the assertion instance", () => {
210+
const { getByText } = render(<HaveClassTestComponent className="foo bar" />);
211+
const divTest = getByText("Test text inside a div");
212+
const test = new ElementAssertion(divTest);
213+
214+
expect(test.toHaveAnyClass("bar", "baz")).toBeEqual(test);
215+
216+
expect(() => test.not.toHaveAnyClass("bar", "baz"))
217+
.toThrowError(AssertionError)
218+
.toHaveMessage('Expected the element to NOT have any of these classes: "bar baz"');
219+
});
220+
});
221+
222+
context("when the element does not have any of the expected classes", () => {
223+
it("throws an assertion error", () => {
224+
const { getByText } = render(<HaveClassTestComponent className="foo" />);
225+
const divTest = getByText("Test text inside a div");
226+
const test = new ElementAssertion(divTest);
227+
228+
expect(() => test.toHaveAnyClass("bar", "baz"))
229+
.toThrowError(AssertionError)
230+
.toHaveMessage('Expected the element to have at least one of these classes: "bar baz"');
231+
232+
expect(test.not.toHaveAnyClass("bar", "baz")).toBeEqual(test);
233+
});
234+
});
235+
});
236+
237+
describe(".toHaveAllClasses", () => {
238+
context("when the element has all the expected classes", () => {
239+
it("returns the assertion instance", () => {
240+
const { getByText } = render(<HaveClassTestComponent className="foo bar baz" />);
241+
const divTest = getByText("Test text inside a div");
242+
const test = new ElementAssertion(divTest);
243+
244+
expect(test.toHaveAllClasses("foo", "bar")).toBeEqual(test);
245+
246+
expect(() => test.not.toHaveAllClasses("foo", "bar"))
247+
.toThrowError(AssertionError)
248+
.toHaveMessage('Expected the element to NOT have all of these classes: "foo bar"');
249+
});
250+
});
251+
252+
context("when the element does not have all the expected classes", () => {
253+
it("throws an assertion error", () => {
254+
const { getByText } = render(<HaveClassTestComponent className="foo bar" />);
255+
const divTest = getByText("Test text inside a div");
256+
divTest.classList.add("foo", "bar");
257+
const test = new ElementAssertion(divTest);
258+
259+
expect(() => test.toHaveAllClasses("foo", "bar", "baz"))
260+
.toThrowError(AssertionError)
261+
.toHaveMessage('Expected the element to have all of these classes: "foo bar baz"');
262+
263+
expect(test.not.toHaveAllClasses("foo", "bar", "baz")).toBeEqual(test);
264+
});
265+
});
266+
});
267+
175268
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ReactElement } from "react";
2+
3+
export function HaveClassTestComponent({ className }: { className?: string; }): ReactElement {
4+
return (
5+
<div className={className}>
6+
{"Test text inside a div"}
7+
</div>
8+
);
9+
}

0 commit comments

Comments
 (0)