Skip to content
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
62 changes: 49 additions & 13 deletions packages/dom/src/lib/ElementAssertion.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Assertion, AssertionError } from "@assertive-ts/core";
import equal from "fast-deep-equal";

import { getReceivedStyle, normalizeStyles } from "./helpers/helpers";
import { getExpectedAndReceivedStyles } from "./helpers/helpers";

export class ElementAssertion<T extends Element> extends Assertion<T> {

Expand Down Expand Up @@ -158,20 +158,12 @@ export class ElementAssertion<T extends Element> extends Assertion<T> {
*/

public toHaveStyle(expected: Partial<CSSStyleDeclaration>): this {
if (!this.actual.ownerDocument.defaultView) {
throw new Error("The element is not attached to a document with a default view.");
}
if (!(this.actual instanceof HTMLElement)) {
throw new Error("The element is not an HTMLElement.");
}

const window = this.actual.ownerDocument.defaultView;

const received = window.getComputedStyle(this.actual);
const [expectedStyle, receivedStyle] = getExpectedAndReceivedStyles(this.actual, expected);

const { props, expectedStyle } = normalizeStyles(expected);

const receivedStyle = getReceivedStyle(props, received);
if (!expectedStyle || !receivedStyle) {
throw new Error("Currently there are no available styles.");
}

const error = new AssertionError({
actual: this.actual,
Expand All @@ -190,6 +182,50 @@ export class ElementAssertion<T extends Element> extends Assertion<T> {
});
}

/**
* Asserts that the element has one or more of the specified CSS styles.
*
* @example
* ```
* expect(component).toHaveSomeStyle({ color: 'green', display: 'block' });
* ```
*
* @param expected the expected CSS style/s.
* @returns the assertion instance.
*/

public toHaveSomeStyle(expected: Partial<CSSStyleDeclaration>): this {

const [expectedStyle, elementProcessedStyle] = getExpectedAndReceivedStyles(this.actual, expected);

if (!expectedStyle || !elementProcessedStyle) {
throw new Error("No available styles.");
}

const hasSomeStyle = Object.entries(expectedStyle).some(([expectedProp, expectedValue]) => {
return Object.entries(elementProcessedStyle).some(([receivedProp, receivedValue]) => {
return equal(expectedProp, receivedProp) && equal(expectedValue, receivedValue);
});
});

const error = new AssertionError({
actual: this.actual,
message: `Expected the element to match some of the following styles:\n${JSON.stringify(expectedStyle, null, 2)}`,
});

const invertedError = new AssertionError({
actual: this.actual,
// eslint-disable-next-line max-len
message: `Expected the element NOT to match some of the following styles:\n${JSON.stringify(expectedStyle, null, 2)}`,
});

return this.execute({
assertWhen: hasSomeStyle,
error,
invertedError,
});
}

/**
* Helper method to assert the presence or absence of class names.
*
Expand Down
33 changes: 30 additions & 3 deletions packages/dom/src/lib/helpers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface StyleDeclaration extends Record<string, string> {
value: string;
}

export const normalizeStyles = (css: Partial<CSSStyleDeclaration>):
const normalizeStyles = (css: Partial<CSSStyleDeclaration>):
{ expectedStyle: StyleDeclaration; props: string[]; } => {
const normalizer = document.createElement("div");
document.body.appendChild(normalizer);
Expand Down Expand Up @@ -48,9 +48,36 @@ export const normalizeStyles = (css: Partial<CSSStyleDeclaration>):
return { expectedStyle, props };
};

export const getReceivedStyle = (props: string[], received: CSSStyleDeclaration): StyleDeclaration => {
const getReceivedStyle = (props: string[], received: CSSStyleDeclaration): StyleDeclaration => {
return props.reduce((acc, prop) => {
acc[prop] = received?.getPropertyValue(prop).trim();
const actualStyle = received.getPropertyValue(prop).trim();
if (!actualStyle) {
return acc;
}
acc[prop] = actualStyle;
return acc;
}, {} as StyleDeclaration);
};

export const getExpectedAndReceivedStyles =
(actual: Element, expected: Partial<CSSStyleDeclaration>): StyleDeclaration[] => {
if (!actual.ownerDocument.defaultView) {
throw new Error("The element is not attached to a document with a default view.");
}
if (!(actual instanceof HTMLElement)) {
throw new Error("The element is not an HTMLElement.");
}

const window = actual.ownerDocument.defaultView;

const rawElementStyles = window.getComputedStyle(actual);

const { props, expectedStyle } = normalizeStyles(expected);

const elementProcessedStyle = getReceivedStyle(props, rawElementStyles);

return [
expectedStyle,
elementProcessedStyle,
];
};
43 changes: 43 additions & 0 deletions packages/dom/test/unit/lib/ElementAssertion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -337,4 +337,47 @@ describe("[Unit] ElementAssertion.test.ts", () => {
});
});
});

describe(".toHaveSomeStyle", () => {
context("when the element contains one or more expected styles", () => {
it("returns the assertion instance", () => {
const { getByTestId } = render(
<div
style={{ color: "blue", maxHeight: "3rem", width: "2rem" }}
data-testid="test-div"
/>,
);
const divTest = getByTestId("test-div");
const test = new ElementAssertion(divTest);

expect(test.toHaveSomeStyle({ color: "red", display: "flex", height: "3rem", width: "2rem" })).toBeEqual(test);

expect(() => test.not.toHaveSomeStyle({ color: "blue" }))
.toThrowError(AssertionError)
// eslint-disable-next-line max-len
.toHaveMessage("Expected the element NOT to match some of the following styles:\n{\n \"color\": \"rgb(0, 0, 255)\"\n}");
});
});

context("when the element does not contain any of the expected styles", () => {
it("throws an assertion error", () => {
const { getByTestId } = render(
<div
className="foo bar test"
style={{ border: "1px solid black", color: "blue", display: "block" }}
data-testid="test-div"
/>,
);
const divTest = getByTestId("test-div");
const test = new ElementAssertion(divTest);

expect(() => test.toHaveSomeStyle({ color: "red", display: "flex" }))
.toThrowError(AssertionError)
// eslint-disable-next-line max-len
.toHaveMessage("Expected the element to match some of the following styles:\n{\n \"color\": \"rgb(255, 0, 0)\",\n \"display\": \"flex\"\n}");

expect(test.not.toHaveSomeStyle({ border: "1px solid blue", color: "red", display: "flex" })).toBeEqual(test);
});
});
});
});