Skip to content

feat(native): add toHaveStyle #152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: feat/native-to-have-prop
Choose a base branch
from
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
43 changes: 43 additions & 0 deletions packages/native/src/lib/ElementAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { get } from "dot-prop-immutable";
import { ReactTestInstance } from "react-test-renderer";

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

export class ElementAssertion extends Assertion<ReactTestInstance> {
public constructor(actual: ReactTestInstance) {
Expand Down Expand Up @@ -200,6 +202,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 ${JSON.stringify(flattenedStyle)}.`,
});

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

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

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

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

export function getFlattenedStyle(style: AssertiveStyle): StyleObject {
const flattenedStyle = StyleSheet.flatten(style);
return flattenedStyle ? (flattenedStyle as StyleObject) : {};
}
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 {\"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 {\"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 {\"backgroundColor\":\"red\",\"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 {\"backgroundColor\":\"red\",\"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 {\"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 {\"backgroundColor\":\"red\",\"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 {\"backgroundColor\":\"red\",\"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 {\"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 {\"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 {\"backgroundColor\":\"red\"}.");
});
});
});
});