Search code examples
javascriptreactjsjestjslesstesting-library

How do you test the functionality of less CSS with Jest?


I've recently joined a new organisation and they use a lot of CSS to hide/show elements.

First of all, is this good practice? I have always (in most cases) shown and hidden components using a boolean to add or remove it from the dom (this is also easy to test)

Whilst trying to add tests using @testing-library/react I've found that the classes are visible by using the identity-obj-proxy module.

However, when trying to test the functionality of an element being visible or not, it becomes difficult because I don't think the less code is being compiled.

Is it possible to compile less code so it will be reflected in the tests?

Could it be something to do with the classnames module being used?

failing test

  it('should open and close when clicked', async () => {
    render(
      <Collapse
        label="Collapse"
        testId="collapse-test"
        isHidden
      >
        <div>
          <h1>just some demo text</h1>
        </div>
      </Collapse>
    )
    const content = screen.getByTestId('collapse-test-content')
    expect(content).not.toBeVisible()
    userEvent.click(screen.getByTestId('collapse-test-button'))
    await waitFor(() => expect(content).toBeVisible())
  })

====================result====================
expect(element).not.toBeVisible()

    Received element is visible:
      <div aria-expanded="false" class="accordionContent contentHidden" data-testid="collapse-test-content" />

      38 |     )
      39 |     const content = screen.getByTestId('collapse-test-content')
    > 40 |     expect(content).not.toBeVisible()
         |                         ^
      41 |     userEvent.click(screen.getByTestId('collapse-test-button'))
      42 |     await waitFor(() => expect(content).toBeVisible())
      43 |   })

Component

import React, { useState } from 'react'
import cn from 'classnames'
import styles from './styles.less'

const AccordionContent = ({ children, hidden, testId }) => {
  const displayClass = hidden ? styles.contentHidden : styles.contentBlock

  const accordionContentClass = cn(styles.accordionContent, displayClass)
  return (
    <div
      className={ accordionContentClass }
      aria-expanded={ !hidden }
      data-testid={ `${testId}-content` }
    >
      {children}
    </div>
  )
}

const CollapseComponent= ({
  isHidden,
  onClick,
  label,
  children,
  testId
}) => {
  const [hidden, toggleHidden] = useState(isHidden)

  const handleOnpress = () => {
    toggleHidden((curr) => !curr)
    if (onClick) { onClick }
  }

  return (
    <div
      className={ styles.accordionWrapper }
      data-testid={ testId }
    >
      <AccordionButton
        onPress={ handleOnpress }
        buttonLabel={ label }
        testId={ testId }
      />
      <AccordionContent
        hidden={ !!hidden }
        testId={ testId }
      >
        {children}
      </AccordionContent>
    </div>
  )
}

styles.less

.accordion-content {
  background-color: @preservica-gray-1;
  display: flex;
}

.content-hidden {
  display: none;
} 

.content-block {
  display: flex;
}

jest.config

const config = {
  testEnvironment: 'jsdom',
  coverageThreshold: {
    global: {
      statements: 80,
      branches: 75,
      functions: 75,
      lines: 80
    }
  },
  testPathIgnorePatterns: [
    "./src/components/atoms/Icons",
    "./src/models"
  ],
  coveragePathIgnorePatterns: [
    "./src/components/atoms/Icons",
    "./src/models"
  ],
  setupFilesAfterEnv: [
    "<rootDir>/src/setupTests.ts"
  ],
  moduleNameMapper: {
    "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
    "\\.(css|less)$": "identity-obj-proxy",
    "^@root(.*)$": "<rootDir>/src$1",
    "^common(.*)$": "<rootDir>/src/common$1",
    "^translation(.*)$": "<rootDir>/src/translation$1",
    "^view(.*)$": "<rootDir>/src/view$1",
    "^actions(.*)$": "<rootDir>/src/actions$1",
    "^usecases(.*)$": "<rootDir>/src/usecases$1",
    "^repository(.*)$": "<rootDir>/src/repository$1",
    "^models(.*)$": "<rootDir>/src/models$1",
    "^router(.*)$": "<rootDir>/src/router$1",
  },
  transform: {
    "^.+\\.(ts|tsx|js|jsx)$": "ts-jest",
  },
  snapshotSerializers: [
    "enzyme-to-json/serializer"
  ]
}

Solution

  • You can read more on how Jest handles mocking CSS modules in the Jest docs. You could perhaps write your own module name mapper or custom transform to load and process the Less files. However, you'd have to figure out how to actually inject the CSS into the code under test (that's something that Webpack normally handles). Something like jest-transform-css might do this.

    Personally, I'd just test whether the CSS class is present, like @jonrsharpe suggests. Think of it from the perspective of the test pyramid: your Jest tests should likely be focused at the unit test level, with an emphasis on speed and simplicity. Unit tests are ideally fast enough that you can run them nearly instantly, whenever you save a file; adding the complexity to parse and insert Less CSS may work against that.

    It's okay if the unit tests don't test the entire stack; you have other tests, higher up in the pyramid, to do this. For example, you could have a handful of Cypress tests that run your app in the actual browser and verify that a couple of controls are actually hidden, then it should be safe to assume that (1) Jest validating all controls set the correct class plus (2) Cypress validating that a few controls with the correct class are correctly hidden means that (3) all controls are correctly hidden.

    To help make your tests more self-documenting, and to make them easier to maintain if you ever change how controls are shown and hidden, you can use Jest's expect.extend to make your own matcher. Perhaps something like this (untested):

    expect.extend({
      toBeVisibleViaCss(received) {
        const pass = !received.classList.contains('content-hidden');
        const what = pass ? 'not visible' : 'visible';
        return {
          message: () => `expected ${received} to be ${what}`,
          pass,
        };
      },
    });
    

    First of all, is this good practice? I have always (in most cases) shown and hidden components using a boolean to add or remove it from the dom (this is also easy to test).

    Hiding components via CSS is certainly not what I'm used to. Without knowing your codebase, I'd wonder if the developers were used to previous jQuery-style approaches of hiding via manipulating the class lists. The main advantage I'm aware of keeping components always rendered is that you can animate their transitions if you want to. I'm not sure how performance compares; the browser might find it faster to toggle a CSS class than to add or remove an element, but removing the element means that React has less to render, which could help performance.