Skip to content

Latest commit

 

History

History
236 lines (184 loc) · 7.89 KB

Testing.stories.mdx

File metadata and controls

236 lines (184 loc) · 7.89 KB

import { Meta } from '@storybook/addon-docs';

Testing in Stripes Components

Tests can fortify new features from breaking changes, help prevent bugs from being reintroduced, and overall future-proof against an ever-changing ecosystem. As more people get involved and contribute, they can be confident in fixing or introducing changes without breaking existing functionality.

When we write tests, we need to make sure that they are resilient to change as well as being easily understandable should somebody need to add or adjust tests as other features are introduced and bugs are fixed.

Directory structure

Each component's directory should contain a tests directory that contains test files named with the component name, and sometimes a specific test suite name. The tests directory should also contain an often quite useful interactor.js file. For example, the directory structure for <Button> looks like this:

Button
|-Button.js
|-Button.css
|-index.js
|-readme
|-tests
|  |-interactor.js
|  |-Button-test.js

When Should I Write Tests?

Anytime you change or add features, you should be changing or adding tests.

If you are making a change to existing functionality, existing tests should be updated. If there were no existing test to update, tests should be added. If you are adding new features, you should write plenty of tests for those new features so that you can rest assured nobody else (or even yourself) accidentally breaks it in the future.

Anytime you fix a bug, you should be adding new tests or adding to existing tests.

Bugs can obviously be a real pain. Even "simple" bugs can keep popping up repeatedly as seemingly unrelated things change in the code. When you fix a bug, you most likely want it fixed for good. Writing tests or fortifying existing tests makes it easily known if the bug is accidentally introduced again when said tests fail.

Write tests first!!!

If you're writing a feature or fixing a bug, write or change some tests first so that they fail expectedly, or the bug is successfully reproduced. Then as you start adding or making changes, and your tests start passing one-by-one, you know you're headed in the right direction until they all pass when the feature is complete or the bug is fixed.

How Do I Write Tests?

Tests in stripes components are written using Mochaand interactions and assertions are performed via Bigtest interactors

Getting Started

A good place to start is by looking at existing tests. If the component you're working on already has tests, you can add new tests to existing setup hooks, or copy similar sections and change relevant pieces. If the component does not have existing tests, check out another component's tests, like the Button component, and follow the same patterns but specific to the component you're working on.

The patterns themselves can be outlined in a few simple steps:

  1. Do any setup work necessary
  2. Perform any actions on the component that we want to verify
  3. Make assertions against the desired state

Here's a sample of tests from the Button component:

import React from 'react';

// testing tools
import { describe, beforeEach, it } from 'mocha';
import { expect } from 'chai';

// test helpers
import { mount } from '../../../tests/helpers';

// the component to test
import Button from '../Button';

// interactor used to perform actions (more on this later)
import { Button as ButtonInteractor } from '@folio/stripes-testing';

// the actual tests
describe('Button', () => {
  const button = ButtonInteractor();
  let clicked;

  // 1. Do any setup work necessary
  beforeEach(async () => {
    // set to false before each test
    clicked = false;

    // mount our component with props
    await mount(
      <Button onClick={() => { clicked = true; }} id="button-test">
        test
      </Button>
    );
  });

  // make assertions against the initial rendered state
  // interactors allow this through their `is\has` API.

   it('renders a <button> tag', async () => {
      await button.is({ button: true });
    });

  it('renders with default class', async () => {
      await button.perform((e) => expect(e.classList.contains(css.default)).to.be.true);
    });

    it('renders child text as its label', async () => {
      await button.has({ text: 'test' });
    });

    it('has an id on the root element', async () => {
      await button.has({ id: 'button-test' });
    });

  // testing a specific action

  describe('clicking the button', () => {

    // perform the action...
      beforeEach(async () => {
        await button.click();
      });

      it('calls the onClick handler', () => {
        expect(clicked).to.be.true;
      });
    });
});

We use a helper called mount in our first beforeEach hook. This helper will clean up and teardown any previously mounted component, then mount our new component into the DOM for us. We use the async/await syntax to wait for React to mount our component so the tests don't start running too early. Since this helper cleans up previous components on the next call, this allows us to investigate and debug our tests after they run. You can then use Mocha's it.only to isolate a specific test to debug.

Testing Component Interactions

In the previous example, all of our tests reference the button = ButtonInteractor() at the top of the test suite which is imported from [the button's interactor in @folio/stripes-testing] file](https://github.com/folio-org/stripes-testing/blob/master/interactors/button.js).

import HTML from './baseHTML';

export default HTML.extend('button')
  .selector('a[href],button,input[type=button],input[type=submit],input[type=reset],input[type=image],a[role=button],div[role=button]')
  .filters({
    // some buttons don't have attribute href
    href: (el) => el.getAttribute('href') ?? '',
    type: (el) => el.getAttribute('type'),
    icon: (el) => el.getAttribute('icon'),
    button: (el) => el.tagName === 'BUTTON',
    anchor: (el) => el.tagName === 'A',
    default: (el) => el.classList.contains('default'),
    ariaLabel: (el) => el.ariaLabel,
    ariaExpanded: (el) => el.getAttribute('aria-expanded'),
    disabled: {
      apply: (el) => {
        if (el.disabled !== undefined) return el.disabled;
        return el.getAttribute('aria-disabled') === 'true';
      },
      default: false
    }
  });

You should always use an interactor if your tests need to interact with the DOM either by reading attributes, properties, text, classnames, or by sending actions such as click, focus, blur, or change events.

An Interactor is a powerful, customizable, composable, page object. This adds a layer of durability to our suite because when things like classnames or the markup inevitably change, we should only need to update our interactor as opposed to updating each of our tests. Interactors can also be composed by each other, so if another component uses a button, that component's interactor can use the ButtonInteractor too.

Interactors are also convergent and will wait for elements to exist in the DOM before interacting with them. Interactor properties are lazy and do not query for the element until they are accessed. To learn more about what they do, how to create your own interactors, and how to then compose interactors, check out the BigTest Interactor Guides.

Testing localized components

Components that render translated strings will require intl context. This is conveniently provided in the mountWithContext helper.

// instead of importing `mount`, import `mountWithContext`
import { mountWithContext } from '../../../tests/helpers';

// ...

beforeEach(async () => {
  await mountWithContext(
    <Datepicker />
  );
});