Search code examples
reactjsjestjsenzymeradium

How to test if styles are dynamically applied on a React component


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:

Output of 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]
}

Output of 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:

  • Recursively cause all Color() [Object]s to process so they output their rgb() value as a string.
  • Recursively remove all interactive Radium styles (like :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.


Solution

  • 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.