Skip to content
Draft
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
44 changes: 43 additions & 1 deletion packages/native/src/lib/ElementAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Assertion, AssertionError } from "@assertive-ts/core";
import { get } from "dot-prop-immutable";
import { ReactTestInstance } from "react-test-renderer";

import { instanceToString, isEmpty } from "./helpers/helpers";
import { instanceToString, isEmpty, getFlattenedStyle, styleToString } from "./helpers/helpers";
import { AssertiveStyle } from "./helpers/types";

export class ElementAssertion extends Assertion<ReactTestInstance> {
public constructor(actual: ReactTestInstance) {
Expand Down Expand Up @@ -200,6 +201,47 @@ export class ElementAssertion extends Assertion<ReactTestInstance> {
});
}

/**
* Asserts that a component has the specified style(s) applied.
*
* This method supports both single style objects and arrays of style objects.
* It checks if all specified style properties match on the target element.
*
* @example
* ```
* expect(element).toHaveStyle({ backgroundColor: "red" });
* expect(element).toHaveStyle([{ backgroundColor: "red" }]);
* ```
*
* @param style - A style object to check for.
* @returns the assertion instance
*/
public toHaveStyle(style: AssertiveStyle): this {
const stylesOnElement: AssertiveStyle = get(this.actual, "props.style", {});

const flattenedElementStyle = getFlattenedStyle(stylesOnElement);
const flattenedStyle = getFlattenedStyle(style);

const hasStyle = Object.keys(flattenedStyle)
.every(key => flattenedElementStyle[key] === flattenedStyle[key]);

const error = new AssertionError({
actual: this.actual,
message: `Expected element ${this.toString()} to have style: \n${styleToString(flattenedStyle)}`,
});

const invertedError = new AssertionError({
actual: this.actual,
message: `Expected element ${this.toString()} NOT to have style: \n${styleToString(flattenedStyle)}`,
});

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

private isElementDisabled(element: ReactTestInstance): boolean {
const { type } = element;
const elementType = type.toString();
Expand Down
13 changes: 13 additions & 0 deletions packages/native/src/lib/helpers/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { StyleSheet } from "react-native";
import { ReactTestInstance } from "react-test-renderer";

import { AssertiveStyle, StyleObject } from "./types";

/**
* Checks if a value is empty.
*
Expand Down Expand Up @@ -31,3 +34,13 @@ export function instanceToString(instance: ReactTestInstance | null): string {

return `<${instance.type.toString()} ... />`;
}

export function getFlattenedStyle(style: AssertiveStyle): StyleObject {
const flattenedStyle = StyleSheet.flatten(style);
return flattenedStyle ? (flattenedStyle as StyleObject) : {};
}

export function styleToString(flattenedStyle: StyleObject): string {
const styleEntries = Object.entries(flattenedStyle);
return styleEntries.map(([key, value]) => `\t- ${key}: ${String(value)};`).join("\n");
}
19 changes: 19 additions & 0 deletions packages/native/src/lib/helpers/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ImageStyle, StyleProp, TextStyle, ViewStyle } from "react-native";

/**
* Type representing a style that can be applied to a React Native component.
* It can be a style for text, view, or image components.
*/
export type Style = TextStyle | ViewStyle | ImageStyle;

/**
* Type for a style prop that can be applied to a React Native component.
* It can be a single style or an array of styles.
*/
export type AssertiveStyle = StyleProp<Style>;

/**
* Type representing a style object when flattened.
* It is a record where the keys are strings and the values can be of any type.
*/
export type StyleObject = Record<string, unknown>;
147 changes: 147 additions & 0 deletions packages/native/test/lib/ElementAssertion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,4 +375,151 @@ describe("[Unit] ElementAssertion.test.ts", () => {
});
});
});

describe(".toHaveStyle", () => {
context("when the element contains the target style", () => {
it("returns the assertion instance", () => {
const element = render(
<View testID="id" style={{ backgroundColor: "red" }} />,
);
const test = new ElementAssertion(element.getByTestId("id"));

expect(test.toHaveStyle({ backgroundColor: "red" })).toBe(test);
expect(() => test.not.toHaveStyle({ backgroundColor: "red" }))
.toThrowError(AssertionError)
.toHaveMessage("Expected element <View ... /> NOT to have style: \n\t- backgroundColor: red;");
});
});

context("when the element does NOT contain the target style", () => {
it("throws an error", () => {
const element = render(
<View testID="id" style={{ opacity: 1 }} />,
);
const test = new ElementAssertion(element.getByTestId("id"));

expect(test.not.toHaveStyle({ backgroundColor: "red" })).toBeEqual(test);
expect(() => test.toHaveStyle({ backgroundColor: "red" }))
.toThrowError(AssertionError)
.toHaveMessage("Expected element <View ... /> to have style: \n\t- backgroundColor: red;");
});
});

context("when the element contains multiple styles and matches all", () => {
it("returns the assertion instance", () => {
const element = render(
<View testID="id" style={{ backgroundColor: "red", opacity: 0.5 }} />,
);
const test = new ElementAssertion(element.getByTestId("id"));

expect(test.toHaveStyle({ backgroundColor: "red", opacity: 0.5 })).toBe(test);
expect(() => test.not.toHaveStyle({ backgroundColor: "red", opacity: 0.5 }))
.toThrowError(AssertionError)
.toHaveMessage(
"Expected element <View ... /> NOT to have style: \n\t- backgroundColor: red;\n\t- opacity: 0.5;",
);
});
});

context("when the element contains multiple styles but does not match all", () => {
it("throws an error", () => {
const element = render(
<View testID="id" style={{ backgroundColor: "red", opacity: 1 }} />,
);
const test = new ElementAssertion(element.getByTestId("id"));

expect(test.not.toHaveStyle({ backgroundColor: "red", opacity: 0.5 })).toBeEqual(test);
expect(() => test.toHaveStyle({ backgroundColor: "red", opacity: 0.5 }))
.toThrowError(AssertionError)
.toHaveMessage("Expected element <View ... /> to have style: \n\t- backgroundColor: red;\n\t- opacity: 0.5;");
});
});

context("when the element has no style prop", () => {
it("throws an error", () => {
const element = render(
<View testID="id" />,
);
const test = new ElementAssertion(element.getByTestId("id"));

expect(test.not.toHaveStyle({ backgroundColor: "red" })).toBeEqual(test);
expect(() => test.toHaveStyle({ backgroundColor: "red" }))
.toThrowError(AssertionError)
.toHaveMessage("Expected element <View ... /> to have style: \n\t- backgroundColor: red;");
});
});

context("when the element style is an array and contains the target style", () => {
it("returns the assertion instance", () => {
const element = render(
<View testID="id" style={[{ backgroundColor: "red" }, { opacity: 0.5 }]} />,
);
const test = new ElementAssertion(element.getByTestId("id"));

expect(test.toHaveStyle({ backgroundColor: "red", opacity: 0.5 })).toBe(test);
expect(() => test.not.toHaveStyle({ backgroundColor: "red", opacity: 0.5 }))
.toThrowError(AssertionError)
.toHaveMessage(
"Expected element <View ... /> NOT to have style: \n\t- backgroundColor: red;\n\t- opacity: 0.5;",
);
});
});

context("when the passed style is an array and contains the target style", () => {
it("returns the assertion instance", () => {
const element = render(
<View testID="id" style={[{ backgroundColor: "red" }, { opacity: 0.5 }]} />,
);
const test = new ElementAssertion(element.getByTestId("id"));
expect(test.toHaveStyle([{ backgroundColor: "red" }, { opacity: 0.5 }])).toBe(test);
expect(() => test.not.toHaveStyle([{ backgroundColor: "red" }, { opacity: 0.5 }]))
.toThrowError(AssertionError)
.toHaveMessage(
"Expected element <View ... /> NOT to have style: \n\t- backgroundColor: red;\n\t- opacity: 0.5;",
);
});
});

context("when the element style is an array and does not contain the target style", () => {
it("throws an error", () => {
const element = render(
<View testID="id" style={[{ backgroundColor: "blue" }, { opacity: 1 }]} />,
);
const test = new ElementAssertion(element.getByTestId("id"));

expect(test.not.toHaveStyle({ backgroundColor: "red" })).toBeEqual(test);
expect(() => test.toHaveStyle({ backgroundColor: "red" }))
.toThrowError(AssertionError)
.toHaveMessage("Expected element <View ... /> to have style: \n\t- backgroundColor: red;");
});
});

context("when the style value is undefined", () => {
it("throws an error", () => {
const element = render(
<View testID="id" style={{ backgroundColor: undefined }} />,
);
const test = new ElementAssertion(element.getByTestId("id"));

expect(test.not.toHaveStyle({ backgroundColor: "red" })).toBeEqual(test);
expect(() => test.toHaveStyle({ backgroundColor: "red" }))
.toThrowError(AssertionError)
.toHaveMessage("Expected element <View ... /> to have style: \n\t- backgroundColor: red;");
});
});

context("when the style value is null", () => {
it("throws an error", () => {
const element = render(
<View testID="id" style={null} />,
);
const test = new ElementAssertion(element.getByTestId("id"));

expect(test.not.toHaveStyle({ backgroundColor: "red" })).toBeEqual(test);
expect(() => test.toHaveStyle({ backgroundColor: "red" }))
.toThrowError(AssertionError)
.toHaveMessage("Expected element <View ... /> to have style: \n\t- backgroundColor: red;");
});
});
});
});