I've written a React component, Button
:
import PropTypes from 'prop-types'
import Radium from 'radium'
import React from 'react'
import { Icon } from 'components'
import { COLOURS, GLOBAL_STYLES, ICONS, MEASUREMENTS } from 'app-constants'
@Radium
export default class Button extends React.Component {
static propTypes = {
children: PropTypes.string,
dark: PropTypes.bool,
icon: PropTypes.oneOf(Object.values(ICONS)).isRequired,
style: PropTypes.object,
}
render() {
const { children, dark, icon, style } = this.props
let mergedStyles = Object.assign({}, styles.base, style)
if (!children)
mergedStyles.icon.left = 0
if (dark)
mergedStyles = Object.assign(mergedStyles, styles.dark)
return (
<button
className="btn btn-secondary"
style={mergedStyles}
tabIndex={-1}>
<Icon name={icon} style={mergedStyles.icon} />
{children &&
<span style={mergedStyles.text}>{children}</span>
}
</button>
)
}
}
export const styles = {
base: {
backgroundColor: COLOURS.WHITE,
border: `1px solid ${COLOURS.BORDER_LIGHT}`,
borderRadius: GLOBAL_STYLES.BORDER_RADIUS,
cursor: 'pointer',
padding: GLOBAL_STYLES.BUTTON_PADDING,
':focus': {
outline: 'none',
},
':hover': {
boxShadow: GLOBAL_STYLES.BOX_SHADOW,
},
icon: {
fontSize: GLOBAL_STYLES.ICON_SIZE_TINY,
left: '-3px',
verticalAlign: 'middle',
},
text: {
fontSize: GLOBAL_STYLES.FONT_SIZE_TINY,
fontWeight: GLOBAL_STYLES.FONT_2_WEIGHT_MEDIUM,
marginLeft: `${MEASUREMENTS.BUTTON_PADDING.HORIZONTAL}px`,
verticalAlign: 'middle',
},
},
dark: {
backgroundColor: COLOURS.PRIMARY_3,
border: `1px solid ${COLOURS.PRIMARY_2}`,
color: COLOURS.WHITE,
':hover': {
boxShadow: GLOBAL_STYLES.BOX_SHADOW_DARK,
},
},
}
I've also written a test for Button
with Jest and Enzyme, which validates if its dark
styles are applied when its dark
prop is set to true
:
import { ICONS } from 'app-constants'
import Button, { styles } from 'components/Button'
describe("<Button>", () => {
let props
let mountedComponent
const getComponent = () => {
if (!mountedComponent)
mountedComponent = shallow(
<Button {...props} />
)
return mountedComponent
}
beforeEach(() => {
mountedComponent = undefined
props = {
children: undefined,
dark: undefined,
icon: ICONS.VIBE,
style: undefined,
}
})
describe("when `dark` is `true`", () => {
beforeEach(() => {
props.dark = true
})
it("applies the component's `dark` styles", () => {
const componentStyles = getComponent().props().style
expect(componentStyles).toEqual(expect.objectContaining(styles.dark))
})
})
})
As you can see, I do this by checking if the properties of styles.dark
are inside the rendered Button
's style
attribute. If they are, then it means the styles have applied successfully.
The issue is that styles.dark
and componentStyles
don't match:
console.log(styles.dark)
ObjectContaining{
":hover": {
"boxShadow": "0px 0px 0px 2px rgba(0,0,0,0.2)"
},
"backgroundColor": [Object],
"border": "1px solid rgb(47, 52, 63)",
"color": [Object]
}
console.log(componentStyles)
{
"backgroundColor": "rgb(31, 34, 40)",
"border": "1px solid rgb(47, 52, 63)",
"borderRadius": "4px",
"color": "rgb(255, 255, 255)",
"cursor": "pointer",
"padding": "3px 5px 3px 5px"
}
I notice a few issues here:
styles.dark
has several Color()
[Object]
s from the color
library. They haven't outputted their rgb()
value as a string, but the same properties in componentStyles
have, thus resulting in a mismatch.componentStyles
has Radium's interactive styles stripped, such as :focus
and :hover
(I assume Radium does this during rendering triggered by Enzyme's shallow()
function). This causes a mismatch with styles.dark
, which doesn't have these properties stripped.As a result, I'm not sure how to test this. I can't think of any alternative solutions to validate that styles.dark
has been applied. I think that doing the following to styles.dark
during testing would be a solution:
Color()
[Object]
s to process so they output their rgb()
value as a string.:focus
and :hover
)Doing so would cause styles.dark
to equal the value of componentStyles
, thus passing the test. I'm just not sure how to do it.
I came back to this a few days later with fresh eyes and thought of a solution:
describe("<Button>", () => {
let props
let mountedComponent
let defaultComponent
const getComponent = () => {
if (!mountedComponent)
mountedComponent = shallow(
<Button {...props} />
)
return mountedComponent
}
beforeEach(() => {
props = {
children: undefined,
dark: undefined,
icon: ICONS.VIBE,
style: undefined,
}
defaultComponent = getComponent()
mountedComponent = undefined
})
describe("when `dark` is `true`", () => {
beforeEach(() => {
props.dark = true
})
it("applies the component's `dark` styles", () => {
const darkStyles = getComponent().props().style
expect(defaultComponent.props().style).not.toEqual(darkStyles)
})
})
})
Rather than asserting that the rendered component's style
prop contains the styles.dark
(which is brittle), it just checks to see if the styles have changed at all when the dark
prop is set to true
.