diff --git a/README.md b/README.md index f52cc32..03e8351 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ + ## Table of Contents - [The problem](#the-problem) @@ -40,7 +41,7 @@ - [`toContainElement(element)`](#tocontainelementelement) - [`toHaveProp(prop, value)`](#tohavepropprop-value) - [`toHaveTextContent(text)`](#tohavetextcontenttext) -- [Todo list](#todo-list) + - [`toHaveStyle(styles)`](#tohavestylestyles) - [Inspiration](#inspiration) - [Other solutions](#other-solutions) @@ -244,9 +245,33 @@ expect(queryByTestId('count-value')).toHaveTextContent(/2/); expect(queryByTestId('count-value')).not.toHaveTextContent('21'); ``` -## Todo list +### `toHaveStyle(styles)` + +```javascript +toHaveStyle(styles); +``` -- [ ] toHaveStyle(any) {?} +Check if an element has the supplied styles. + +You can pass either an object of React Native style properties, or an array of objects with style +properties. You cannot pass properties from a React Native stylesheet.. + +#### Examples + +```javascript +const styles = StyleSheet.create({ text: { fontSize: 16 } }); + +const { queryByText } = render( + Hello World, +); + +expect(queryByText('Hello World')).toHaveStyle({ color: 'black', fontWeight: '600', fontSize: 16 }); +expect(queryByText('Hello World')).toHaveStyle({ color: 'black' }); +expect(queryByText('Hello World')).toHaveStyle({ fontWeight: '600' }); +expect(queryByText('Hello World')).toHaveStyle({ fontSize: 16 }); +expect(queryByText('Hello World')).toHaveStyle([{ color: 'black' }, { fontWeight: '600' }]); +expect(queryByText('Hello World')).not.toHaveStyle({ color: 'white' }); +``` ## Inspiration diff --git a/src/__tests__/to-have-style.js b/src/__tests__/to-have-style.js new file mode 100644 index 0000000..c18c963 --- /dev/null +++ b/src/__tests__/to-have-style.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { StyleSheet, View, Text } from 'react-native'; +import { render } from 'native-testing-library'; + +describe('.toHaveStyle', () => { + test('handles positive test cases', () => { + const styles = StyleSheet.create({ container: { color: 'white' } }); + const { getByTestId } = render( + + Hello World + , + ); + + const container = getByTestId('container'); + + expect(container).toHaveStyle({ backgroundColor: 'blue', height: '100%' }); + expect(container).toHaveStyle([{ backgroundColor: 'blue' }, { height: '100%' }]); + expect(container).toHaveStyle({ backgroundColor: 'blue' }); + expect(container).toHaveStyle({ height: '100%' }); + expect(container).toHaveStyle({ color: 'white' }); + }); + + test('handles negative test cases', () => { + const { getByTestId } = render( + + Hello World + , + ); + + const container = getByTestId('container'); + + expect(() => expect(container).toHaveStyle({ fontWeight: 'bold' })).toThrowError(); + expect(() => expect(container).not.toHaveStyle({ color: 'black' })).toThrowError(); + }); +}); diff --git a/src/index.js b/src/index.js index 37a4393..e66f7d9 100644 --- a/src/index.js +++ b/src/index.js @@ -3,5 +3,14 @@ import { toBeEmpty } from './to-be-empty'; import { toHaveProp } from './to-have-prop'; import { toHaveTextContent } from './to-have-text-content'; import { toContainElement } from './to-contain-element'; +import { toHaveStyle } from './to-have-style'; -export { toBeDisabled, toContainElement, toBeEmpty, toHaveProp, toHaveTextContent, toBeEnabled }; +export { + toBeDisabled, + toContainElement, + toBeEmpty, + toHaveProp, + toHaveTextContent, + toBeEnabled, + toHaveStyle, +}; diff --git a/src/to-have-style.js b/src/to-have-style.js new file mode 100644 index 0000000..fdba11a --- /dev/null +++ b/src/to-have-style.js @@ -0,0 +1,49 @@ +import { matcherHint } from 'jest-matcher-utils'; +import jestDiff from 'jest-diff'; +import chalk from 'chalk'; +import { all, compose, mergeAll, toPairs } from 'ramda'; + +import { checkReactElement } from './utils'; + +function isSubset(expected, received) { + return compose( + all(([prop, value]) => received[prop] === value), + toPairs, + )(expected); +} + +function printoutStyles(styles) { + return Object.keys(styles) + .sort() + .map(prop => `${prop}: ${styles[prop]};`) + .join('\n'); +} + +// Highlights only style rules that were expected but were not found in the +// received computed styles +function expectedDiff(expected, elementStyles) { + const received = Object.keys(elementStyles) + .filter(prop => expected[prop]) + .reduce((obj, prop) => Object.assign(obj, { [prop]: elementStyles[prop] }), {}); + + const diffOutput = jestDiff(printoutStyles(expected), printoutStyles(received)); + // Remove the "+ Received" annotation because this is a one-way diff + return diffOutput.replace(`${chalk.red('+ Received')}\n`, ''); +} + +export function toHaveStyle(element, style) { + checkReactElement(element, toHaveStyle, this); + + const elementStyle = element.props.style; + + const expected = Array.isArray(style) ? mergeAll(style) : style; + const received = Array.isArray(elementStyle) ? mergeAll(elementStyle) : elementStyle; + + return { + pass: isSubset(expected, received), + message: () => { + const matcher = `${this.isNot ? '.not' : ''}.toHaveStyle`; + return [matcherHint(matcher, 'element', ''), expectedDiff(expected, received)].join('\n\n'); + }, + }; +} diff --git a/src/utils.js b/src/utils.js index d45f2a0..38870ef 100644 --- a/src/utils.js +++ b/src/utils.js @@ -36,6 +36,7 @@ class ReactElementTypeError extends Error { try { withType = printWithType('Received', received, printReceived); } catch (e) {} + /* istanbul ignore next */ this.message = [ matcherHint(`${context.isNot ? '.not' : ''}.${matcherFn.name}`, 'received', ''), '',