Skip to content

Commit 4ece01b

Browse files
marcinkornekmdjastrzebski
authored andcommitted
feat: implement toHaveStyle with tests
1 parent 701433c commit 4ece01b

File tree

6 files changed

+225
-0
lines changed

6 files changed

+225
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`handles negative test cases 1`] = `
4+
"expect(element).toHaveStyle()
5+
6+
- Expected
7+
8+
backgroundColor: blue;
9+
transform: [
10+
{
11+
- "scale": 1
12+
+ "scale": 2
13+
+ },
14+
+ {
15+
+ "rotate": "45deg"
16+
}
17+
];"
18+
`;
19+
20+
exports[`handles transform when transform undefined 1`] = `
21+
"expect(element).toHaveStyle()
22+
23+
- Expected
24+
25+
- transform: [
26+
- {
27+
- "scale": 1
28+
- }
29+
- ];
30+
+ transform: undefined;"
31+
`;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React from 'react';
2+
import { View, Text, StyleSheet, Pressable } from 'react-native';
3+
import { render } from '../..';
4+
import '../extend-expect';
5+
6+
test('handles positive test cases', () => {
7+
const styles = StyleSheet.create({
8+
container: { borderBottomColor: 'white' },
9+
});
10+
const { getByTestId } = render(
11+
<View
12+
testID="container"
13+
style={[
14+
{
15+
backgroundColor: 'blue',
16+
height: '40%',
17+
transform: [{ scale: 2 }, { rotate: '45deg' }],
18+
},
19+
[{ height: '100%' }],
20+
[[{ width: '50%' }]],
21+
styles.container,
22+
]}
23+
>
24+
<Text>Hello World</Text>
25+
</View>
26+
);
27+
28+
const container = getByTestId('container');
29+
30+
expect(container).toHaveStyle({ backgroundColor: 'blue', height: '100%' });
31+
expect(container).toHaveStyle([
32+
{ backgroundColor: 'blue' },
33+
{ height: '100%' },
34+
]);
35+
expect(container).toHaveStyle({ backgroundColor: 'blue' });
36+
expect(container).toHaveStyle({ height: '100%' });
37+
expect(container).toHaveStyle({ borderBottomColor: 'white' });
38+
expect(container).toHaveStyle({ width: '50%' });
39+
expect(container).toHaveStyle([[{ width: '50%' }]]);
40+
expect(container).toHaveStyle({
41+
transform: [{ scale: 2 }, { rotate: '45deg' }],
42+
});
43+
});
44+
45+
test('handles negative test cases', () => {
46+
const { getByTestId } = render(
47+
<View
48+
testID="container"
49+
style={{
50+
backgroundColor: 'blue',
51+
borderBottomColor: 'black',
52+
height: '100%',
53+
transform: [{ scale: 2 }, { rotate: '45deg' }],
54+
}}
55+
>
56+
<Text>Hello World</Text>
57+
</View>
58+
);
59+
60+
const container = getByTestId('container');
61+
expect(() =>
62+
expect(container).toHaveStyle({
63+
backgroundColor: 'blue',
64+
transform: [{ scale: 1 }],
65+
})
66+
).toThrowErrorMatchingSnapshot();
67+
expect(container).not.toHaveStyle({ fontWeight: 'bold' });
68+
expect(container).not.toHaveStyle({ color: 'black' });
69+
expect(container).not.toHaveStyle({
70+
transform: [{ rotate: '45deg' }, { scale: 2 }],
71+
});
72+
expect(container).not.toHaveStyle({ transform: [{ rotate: '45deg' }] });
73+
});
74+
75+
test('handles when the style prop is undefined', () => {
76+
const { getByTestId } = render(
77+
<View testID="container">
78+
<Text>Hello World</Text>
79+
</View>
80+
);
81+
82+
const container = getByTestId('container');
83+
84+
expect(container).not.toHaveStyle({ fontWeight: 'bold' });
85+
});
86+
87+
test('handles transform when transform undefined', () => {
88+
const { getByTestId } = render(
89+
<View
90+
testID="container"
91+
style={{
92+
backgroundColor: 'blue',
93+
transform: undefined,
94+
}}
95+
>
96+
<Text>Hello World</Text>
97+
</View>
98+
);
99+
100+
const container = getByTestId('container');
101+
expect(() =>
102+
expect(container).toHaveStyle({ transform: [{ scale: 1 }] })
103+
).toThrowErrorMatchingSnapshot();
104+
});
105+
106+
test('handles Pressable with function style prop', () => {
107+
const { getByTestId } = render(
108+
<Pressable testID="test" style={() => ({ backgroundColor: 'blue' })} />
109+
);
110+
expect(getByTestId('test')).toHaveStyle({ backgroundColor: 'blue' });
111+
});

src/matchers/extend-expect.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface JestNativeMatchers<R> {
1212
toBeVisible(): R;
1313
toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R;
1414
toHaveProp(name: string, expectedValue?: unknown): R;
15+
toHaveStyle(style: StyleProp<Style>): R;
1516
toHaveTextContent(expectedText: TextMatch, options?: TextMatchOptions): R;
1617
}
1718

src/matchers/extend-expect.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { toBeSelected } from './to-be-selected';
1010
import { toBeVisible } from './to-be-visible';
1111
import { toHaveDisplayValue } from './to-have-display-value';
1212
import { toHaveProp } from './to-have-prop';
13+
import { toHaveStyle } from './to-have-style';
1314
import { toHaveTextContent } from './to-have-text-content';
1415

1516
expect.extend({
@@ -24,5 +25,6 @@ expect.extend({
2425
toBeVisible,
2526
toHaveDisplayValue,
2627
toHaveProp,
28+
toHaveStyle,
2729
toHaveTextContent,
2830
});

src/matchers/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ export { toBePartiallyChecked } from './to-be-partially-checked';
77
export { toBeVisible } from './to-be-visible';
88
export { toHaveDisplayValue } from './to-have-display-value';
99
export { toHaveProp } from './to-have-prop';
10+
export { toHaveStyle } from './to-have-style';
1011
export { toHaveTextContent } from './to-have-text-content';
1112
export { toBeSelected } from './to-be-selected';

src/matchers/to-have-style.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { ReactTestInstance } from 'react-test-renderer';
2+
import { matcherHint, diff } from 'jest-matcher-utils';
3+
import {
4+
ImageStyle,
5+
StyleProp,
6+
StyleSheet,
7+
TextStyle,
8+
ViewStyle,
9+
} from 'react-native';
10+
import chalk from 'chalk'; // eslint-disable-line import/no-extraneous-dependencies
11+
import { checkHostElement } from './utils';
12+
13+
export type Style = TextStyle | ViewStyle | ImageStyle;
14+
type StyleLike = Record<string, unknown>;
15+
16+
function printoutStyles(style: StyleLike) {
17+
return Object.keys(style)
18+
.sort()
19+
.map((prop) =>
20+
Array.isArray(style[prop])
21+
? `${prop}: ${JSON.stringify(style[prop], null, 2)};`
22+
: `${prop}: ${style[prop]};`
23+
)
24+
.join('\n');
25+
}
26+
27+
/**
28+
* Narrows down the properties in received to those with counterparts in expected
29+
*/
30+
function narrow(expected: StyleLike, received: StyleLike) {
31+
return Object.keys(received)
32+
.filter((prop) => expected[prop])
33+
.reduce(
34+
(obj, prop) =>
35+
Object.assign(obj, {
36+
[prop]: received[prop],
37+
}),
38+
{}
39+
);
40+
}
41+
42+
// Highlights only style rules that were expected but were not found in the
43+
// received computed styles
44+
function expectedDiff(expected: StyleLike, received: StyleLike) {
45+
const receivedNarrow = narrow(expected, received);
46+
47+
const diffOutput = diff(
48+
printoutStyles(expected),
49+
printoutStyles(receivedNarrow)
50+
);
51+
// Remove the "+ Received" annotation because this is a one-way diff
52+
return diffOutput?.replace(`${chalk.red('+ Received')}\n`, '') ?? '';
53+
}
54+
55+
export function toHaveStyle(
56+
this: jest.MatcherContext,
57+
element: ReactTestInstance,
58+
style: StyleProp<Style>
59+
) {
60+
checkHostElement(element, toHaveStyle, this);
61+
62+
const expected = (StyleSheet.flatten(style) ?? {}) as StyleLike;
63+
const received = (StyleSheet.flatten(element.props.style) ?? {}) as StyleLike;
64+
65+
const pass = Object.entries(expected).every(([prop, value]) =>
66+
this.equals(received?.[prop], value)
67+
);
68+
69+
return {
70+
pass,
71+
message: () => {
72+
const matcher = `${this.isNot ? '.not' : ''}.toHaveStyle`;
73+
return [
74+
matcherHint(matcher, 'element', ''),
75+
expectedDiff(expected, received),
76+
].join('\n\n');
77+
},
78+
};
79+
}

0 commit comments

Comments
 (0)