Skip to content

Commit f1ba82a

Browse files
committed
Merge branch 'main' into feat/upgrade-nodejs
2 parents 47a5d31 + c627ade commit f1ba82a

File tree

4 files changed

+354
-4
lines changed

4 files changed

+354
-4
lines changed

packages/dom/src/lib/ElementAssertion.ts

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { Assertion, AssertionError } from "@assertive-ts/core";
2+
import equal from "fast-deep-equal";
3+
4+
import { getExpectedAndReceivedStyles } from "./helpers/helpers";
25

36
export class ElementAssertion<T extends Element> extends Assertion<T> {
47
public constructor(actual: T) {
@@ -141,8 +144,119 @@ export class ElementAssertion<T extends Element> extends Assertion<T> {
141144
);
142145
}
143146

144-
private getClassList(): string[] {
145-
return this.actual.className.split(/\s+/).filter(Boolean);
147+
/**
148+
* Check if the provided element is currently focused in the document.
149+
*
150+
* @example
151+
* const userNameInput = document.querySelector('#username');
152+
* userNameInput.focus();
153+
* expect(userNameInput).toHaveFocus(); // passes
154+
* expect(userNameInput).not.toHaveFocus(); // fails
155+
*
156+
* @returns The assertion instance.
157+
*/
158+
public toHaveFocus(): this {
159+
160+
const hasFocus = this.actual === document.activeElement;
161+
162+
const error = new AssertionError({
163+
actual: this.actual,
164+
expected: document.activeElement,
165+
message: "Expected the element to be focused",
166+
});
167+
168+
const invertedError = new AssertionError({
169+
actual: this.actual,
170+
expected: document.activeElement,
171+
message: "Expected the element NOT to be focused",
172+
});
173+
174+
return this.execute({
175+
assertWhen: hasFocus,
176+
error,
177+
invertedError,
178+
});
179+
}
180+
181+
/**
182+
* Asserts that the element has the specified CSS styles.
183+
*
184+
* @example
185+
* ```
186+
* expect(component).toHaveStyle({ color: 'green', display: 'block' });
187+
* ```
188+
*
189+
* @param expected the expected CSS styles.
190+
* @returns the assertion instance.
191+
*/
192+
193+
public toHaveStyle(expected: Partial<CSSStyleDeclaration>): this {
194+
195+
const [expectedStyle, receivedStyle] = getExpectedAndReceivedStyles(this.actual, expected);
196+
197+
if (!expectedStyle || !receivedStyle) {
198+
throw new Error("Currently there are no available styles.");
199+
}
200+
201+
const error = new AssertionError({
202+
actual: this.actual,
203+
expected: expectedStyle,
204+
message: `Expected the element to match the following style:\n${JSON.stringify(expectedStyle, null, 2)}`,
205+
});
206+
const invertedError = new AssertionError({
207+
actual: this.actual,
208+
message: `Expected the element to NOT match the following style:\n${JSON.stringify(expectedStyle, null, 2)}`,
209+
});
210+
211+
return this.execute({
212+
assertWhen: equal(expectedStyle, receivedStyle),
213+
error,
214+
invertedError,
215+
});
216+
}
217+
218+
/**
219+
* Asserts that the element has one or more of the specified CSS styles.
220+
*
221+
* @example
222+
* ```
223+
* expect(component).toHaveSomeStyle({ color: 'green', display: 'block' });
224+
* ```
225+
*
226+
* @param expected the expected CSS style/s.
227+
* @returns the assertion instance.
228+
*/
229+
230+
public toHaveSomeStyle(expected: Partial<CSSStyleDeclaration>): this {
231+
232+
const [expectedStyle, elementProcessedStyle] = getExpectedAndReceivedStyles(this.actual, expected);
233+
234+
if (!expectedStyle || !elementProcessedStyle) {
235+
throw new Error("No available styles.");
236+
}
237+
238+
const hasSomeStyle = Object.entries(expectedStyle).some(([expectedProp, expectedValue]) => {
239+
return Object.entries(elementProcessedStyle).some(([receivedProp, receivedValue]) => {
240+
return equal(expectedProp, receivedProp) && equal(expectedValue, receivedValue);
241+
});
242+
});
243+
244+
const error = new AssertionError({
245+
actual: this.actual,
246+
message: `Expected the element to match some of the following styles:\n${JSON.stringify(expectedStyle, null, 2)}`,
247+
});
248+
249+
const invertedError = new AssertionError({
250+
actual: this.actual,
251+
// eslint-disable-next-line max-len
252+
message: `Expected the element NOT to match some of the following styles:\n${JSON.stringify(expectedStyle, null, 2)}`,
253+
});
254+
255+
return this.execute({
256+
assertWhen: hasSomeStyle,
257+
error,
258+
invertedError,
259+
});
146260
}
147261

148262
/**
@@ -180,4 +294,8 @@ export class ElementAssertion<T extends Element> extends Assertion<T> {
180294
invertedError,
181295
});
182296
}
297+
298+
private getClassList(): string[] {
299+
return this.actual.className.split(/\s+/).filter(Boolean);
300+
}
183301
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
interface StyleDeclaration extends Record<string, string> {
2+
property: string;
3+
value: string;
4+
}
5+
6+
function normalizeStyles(css: Partial<CSSStyleDeclaration>): StyleDeclaration {
7+
const normalizer = document.createElement("div");
8+
document.body.appendChild(normalizer);
9+
10+
const { expectedStyle } = Object.entries(css).reduce(
11+
(acc, [property, value]) => {
12+
13+
if (typeof value !== "string") {
14+
return acc;
15+
}
16+
17+
normalizer.style.setProperty(property, value);
18+
19+
const normalizedValue = window
20+
.getComputedStyle(normalizer)
21+
.getPropertyValue(property)
22+
.trim();
23+
24+
return {
25+
expectedStyle: {
26+
...acc.expectedStyle,
27+
[property]: normalizedValue,
28+
},
29+
};
30+
},
31+
{ expectedStyle: {} as StyleDeclaration },
32+
);
33+
34+
document.body.removeChild(normalizer);
35+
36+
return expectedStyle;
37+
}
38+
39+
function getReceivedStyle (props: string[], received: CSSStyleDeclaration): StyleDeclaration {
40+
41+
return props.reduce((acc, prop) => {
42+
43+
const actualStyle = received.getPropertyValue(prop).trim();
44+
45+
return actualStyle
46+
? { ...acc, [prop]: actualStyle }
47+
: acc;
48+
49+
}, {} as StyleDeclaration);
50+
}
51+
52+
export const getExpectedAndReceivedStyles =
53+
(actual: Element, expected: Partial<CSSStyleDeclaration>): StyleDeclaration[] => {
54+
if (!actual.ownerDocument.defaultView) {
55+
throw new Error("The element is not attached to a document with a default view.");
56+
}
57+
if (!(actual instanceof HTMLElement)) {
58+
throw new Error("The element is not an HTMLElement.");
59+
}
60+
61+
const window = actual.ownerDocument.defaultView;
62+
63+
const rawElementStyles = window.getComputedStyle(actual);
64+
65+
const expectedStyle = normalizeStyles(expected);
66+
67+
const styleKeys = Object.keys(expectedStyle);
68+
69+
const elementProcessedStyle = getReceivedStyle(styleKeys, rawElementStyles);
70+
71+
return [
72+
expectedStyle,
73+
elementProcessedStyle,
74+
];
75+
};

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

Lines changed: 149 additions & 2 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 { FocusTestComponent } from "./fixtures/focusTestComponent";
67
import { HaveClassTest } from "./fixtures/HaveClassTest";
78
import { NestedElementsTest } from "./fixtures/NestedElementsTest";
89
import { SimpleTest } from "./fixtures/SimpleTest";
@@ -257,11 +258,157 @@ describe("[Unit] ElementAssertion.test.ts", () => {
257258
const test = new ElementAssertion(divTest);
258259

259260
expect(() => test.toHaveAllClasses("foo", "bar", "baz"))
260-
.toThrowError(AssertionError)
261-
.toHaveMessage('Expected the element to have all of these classes: "foo bar baz"');
261+
.toThrowError(AssertionError)
262+
.toHaveMessage('Expected the element to have all of these classes: "foo bar baz"');
262263

263264
expect(test.not.toHaveAllClasses("foo", "bar", "baz")).toBeEqual(test);
264265
});
265266
});
266267
});
268+
269+
describe(".toHaveFocus", () => {
270+
context("when the element has focus", () => {
271+
it("returns the assertion instance", () => {
272+
const { getByTestId } = render(<FocusTestComponent />);
273+
const input1 = getByTestId("input1");
274+
input1.focus();
275+
const test = new ElementAssertion(input1);
276+
277+
expect(test.toHaveFocus()).toBeEqual(test);
278+
279+
expect(() => test.not.toHaveFocus())
280+
.toThrowError(AssertionError)
281+
.toHaveMessage("Expected the element NOT to be focused");
282+
});
283+
});
284+
285+
context("when the element does not have focus", () => {
286+
it("throws an assertion error", () => {
287+
const { getByTestId } = render(<FocusTestComponent />);
288+
const input1 = getByTestId("input1");
289+
const test = new ElementAssertion(input1);
290+
291+
expect(() => test.toHaveFocus())
292+
.toThrowError(AssertionError)
293+
.toHaveMessage("Expected the element to be focused");
294+
295+
expect(test.not.toHaveFocus()).toBeEqual(test);
296+
});
297+
});
298+
});
299+
describe(".toHaveStyle", () => {
300+
context("when the element has the expected style", () => {
301+
it("returns the assertion instance", () => {
302+
const { getByTestId } = render(
303+
<div
304+
className="foo bar test"
305+
style={{ border: "1px solid black", color: "red", display: "flex" }}
306+
data-testid="test-div"
307+
/>);
308+
const divTest = getByTestId("test-div");
309+
const test = new ElementAssertion(divTest);
310+
311+
expect(test.toHaveStyle({ border: "1px solid black", color: "red", display: "flex" })).toBeEqual(test);
312+
313+
expect(() => test.not.toHaveStyle({ border: "1px solid black", color: "red", display: "flex" }))
314+
.toThrowError(AssertionError)
315+
.toHaveMessage(
316+
// eslint-disable-next-line max-len
317+
'Expected the element to NOT match the following style:\n{\n "border": "1px solid black",\n "color": "rgb(255, 0, 0)",\n "display": "flex"\n}',
318+
);
319+
});
320+
});
321+
322+
context("when the element does not have the expected style", () => {
323+
it("throws an assertion error", () => {
324+
const { getByTestId } = render(
325+
<div
326+
className="foo bar test"
327+
style={{ color: "blue", display: "block" }}
328+
data-testid="test-div"
329+
/>,
330+
);
331+
332+
const divTest = getByTestId("test-div");
333+
const test = new ElementAssertion(divTest);
334+
335+
expect(() => test.toHaveStyle(({ border: "1px solid black", color: "red", display: "flex" })))
336+
.toThrowError(AssertionError)
337+
.toHaveMessage(
338+
// eslint-disable-next-line max-len
339+
'Expected the element to match the following style:\n{\n "border": "1px solid black",\n "color": "rgb(255, 0, 0)",\n "display": "flex"\n}',
340+
);
341+
342+
expect(test.not.toHaveStyle({ border: "1px solid black", color: "red", display: "flex" })).toBeEqual(test);
343+
344+
});
345+
});
346+
context("when the element partially match the style", () => {
347+
it("throws an assertion error", () => {
348+
const { getByTestId } = render(
349+
<div
350+
className="foo bar test"
351+
style={{ border: "1px solid black", color: "blue", display: "block" }}
352+
data-testid="test-div"
353+
/>,
354+
);
355+
356+
const divTest = getByTestId("test-div");
357+
const test = new ElementAssertion(divTest);
358+
359+
expect(() => test.toHaveStyle(({ color: "red", display: "flex" })))
360+
.toThrowError(AssertionError)
361+
.toHaveMessage(
362+
// eslint-disable-next-line max-len
363+
'Expected the element to match the following style:\n{\n "color": "rgb(255, 0, 0)",\n "display": "flex"\n}',
364+
);
365+
366+
expect(test.not.toHaveStyle({ border: "1px solid black", color: "red", display: "flex" })).toBeEqual(test);
367+
368+
});
369+
});
370+
});
371+
372+
describe(".toHaveSomeStyle", () => {
373+
context("when the element contains one or more expected styles", () => {
374+
it("returns the assertion instance", () => {
375+
const { getByTestId } = render(
376+
<div
377+
style={{ color: "blue", maxHeight: "3rem", width: "2rem" }}
378+
data-testid="test-div"
379+
/>,
380+
);
381+
const divTest = getByTestId("test-div");
382+
const test = new ElementAssertion(divTest);
383+
384+
expect(test.toHaveSomeStyle({ color: "red", display: "flex", height: "3rem", width: "2rem" })).toBeEqual(test);
385+
386+
expect(() => test.not.toHaveSomeStyle({ color: "blue" }))
387+
.toThrowError(AssertionError)
388+
// eslint-disable-next-line max-len
389+
.toHaveMessage("Expected the element NOT to match some of the following styles:\n{\n \"color\": \"rgb(0, 0, 255)\"\n}");
390+
});
391+
});
392+
393+
context("when the element does not contain any of the expected styles", () => {
394+
it("throws an assertion error", () => {
395+
const { getByTestId } = render(
396+
<div
397+
className="foo bar test"
398+
style={{ border: "1px solid black", color: "blue", display: "block" }}
399+
data-testid="test-div"
400+
/>,
401+
);
402+
const divTest = getByTestId("test-div");
403+
const test = new ElementAssertion(divTest);
404+
405+
expect(() => test.toHaveSomeStyle({ color: "red", display: "flex" }))
406+
.toThrowError(AssertionError)
407+
// eslint-disable-next-line max-len
408+
.toHaveMessage("Expected the element to match some of the following styles:\n{\n \"color\": \"rgb(255, 0, 0)\",\n \"display\": \"flex\"\n}");
409+
410+
expect(test.not.toHaveSomeStyle({ border: "1px solid blue", color: "red", display: "flex" })).toBeEqual(test);
411+
});
412+
});
413+
});
267414
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ReactElement } from "react";
2+
3+
export function FocusTestComponent(): ReactElement {
4+
return (
5+
<div>
6+
<input data-testid="input1" />
7+
<input data-testid="input2" />
8+
</div>
9+
);
10+
}

0 commit comments

Comments
 (0)