Skip to content

Commit

Permalink
docs: add EUI testing docs (#7422)
Browse files Browse the repository at this point in the history
  • Loading branch information
tkajtoch committed Jan 22, 2024
1 parent a9dc1dc commit 5ff0212
Show file tree
Hide file tree
Showing 25 changed files with 844 additions and 2 deletions.
3 changes: 3 additions & 0 deletions website/docs/01_guidelines/testing/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
link:
type: doc
id: testing-introduction
76 changes: 76 additions & 0 deletions website/docs/01_guidelines/testing/introduction.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
title: Testing
slug: /guidelines/testing
id: testing-introduction
---

<p style={{ fontSize: '22px' }}>
Learn how we test our code internally and how you should test integration with EUI components to ensure
good test coverage and easy maintainability.
</p>

## How we test our components

We maintain a large amount of integration and end-to-end tests suites for every component we develop.
They ensure high coverage of most paths within our components, including accessibility features, and lets us know
whenever we change something that may affect your applications.

Internally, we use [Enzyme](https://enzymejs.github.io/enzyme/)
and [React Testing Library](https://testing-library.com/) for integration testing,
[Cypress](https://www.cypress.io/) for end-to-end component testing, as well as static code analysis
and type checking tools that run automatically on every pull request. You can find them all in our
[GitHub repository](https://github.com/elastic/eui).

All of these tests, including a manual code review and QA steps must pass before the changes are merged
to confirm they're up to our standards.

## How should you test views using EUI components?

Testing integration with 3rd-party UI libraries like EUI is slightly different from testing first-party components.
In general, just like with any other dependency, you should not test implementation details like the exact
DOM structure or class names. They're likely to change between versions and usually don't prove a component works
correctly anyway, so it's best to skip these.

Instead, you should **focus on testing your code and the integration with EUI components first**.

Depending on your testing stack, **integration tests** might be written using React Testing Library (RTL),
and are meant to be low-level tests of components working together as expected. For example, if your component fetches
and displays processed data a certain way whenever an `<EuiButton />` is clicked, you should write
a test that simulates a click on that button and verifies the data is actually fetched and displayed the way
you expect it to. Doing this will confirm your code integrates with `<EuiButton />` correctly by passing
the right `onClick` handler, and so that your handler function fetches and updates the data to display the right way.

On top of that, we also recommend writing **end-to-end (e2e) tests** that go through the flow just like a real user
would. In addition to testing what integration tests do, they ensure a browser loads and executes your code properly
as a whole, reacts to real user actions, makes needed network and API calls, and way more. You can use frameworks
like [Cypress](https://cypress.io), [Selenium](https://www.selenium.dev/), [Playwright](https://playwright.dev/),
and more to write end-to-end tests for your application.

When written properly, integration and end-to-end tests will validate if the code you're shipping actually works for
end users, and that the changes you introduce over time don't break the application. This means you can iterate
faster and spend less time on manual testing.

**Writing good tests isn't easy**. This is why we've prepared a set of general and component-specific guidelines
to help you in that process, and we strongly encourage you to read them.
In case these aren't enough, we're here to help! Please reach out to us by opening a GitHub
[issue](https://github.com/elastic/eui/issues/new/choose) or
[discussion](https://github.com/elastic/eui/discussions/new/choose).

:::info Elastic employees
If you're an Elastic employee, we recommend reaching out on the **#eui** Slack channel first.
:::

### Choosing the right selectors

Whenever writing any kind of UI tests, choosing right selectors is the key to making tests reliable long-term.
We recommend using the `data-test-subj` attributes (e.g., `[data-test-subj="comboBoxSearchInput"]`),
ARIA `role` attributes (e.g., `[role="dialog"]`),
and other [semantic queries](https://testing-library.com/docs/queries/about/#priority) for selectors whenever possible
to ensure they reference the same underlying element between versions.

You can find the list of available `data-test-subj` and other attributes as well as what semantic query selectors
we recommend to use in component-specific testing documentation pages.

If you need to use a custom, non-semantic selector (e.g., `div > span.title` or `span:first-child`)
that's not a one-off, please open a [GitHub issue](https://github.com/elastic/eui/issues/new/choose), so we can add it,
or even better so - try adding it yourself and contribute to EUI by opening a pull request! 🎉
136 changes: 136 additions & 0 deletions website/docs/01_guidelines/testing/recommendations.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
title: Testing recommendations
sidebar_label: Recommendations
---

<p style={{ fontSize: '22px' }}>
Our general set of do's and don'ts for testing components and views.
</p>

## Choose the right selectors

Follow RTL's [Guiding Principles](https://testing-library.com/docs/guiding-principles/)
and [query priorities](https://testing-library.com/docs/queries/about/#priority) when choosing right element selectors.

Prioritize accessible and semantic queries (e.g., `[role="dialog"]`) followed by `data-test-subj` attributes over
other, more complicated and prone to breaking queries (e.g. `div > span.title`) whenever possible.

Check out our component-specific testing docs to find the selectors we officially support.

**Do:**
```js
screen.getByRole('dialog'); // react-testing-library
cy.get('[role="dialog"]'); // cypress
driver.findElement(By.cssSelector('[role="dialog"]')); // selenium
```

**Don't:**
```js
container.querySelector('.euiFlyout'); // react-testing-library
cy.get('.euiFlyout'); // cypress
driver.findElement(By.cssSelector('.euiFlyout')); // selenium
```

## Don't use snapshots

**The EUI team strongly discourages snapshot testing**, despite its simplicity.
Snapshot tests are prone to frequent failures due to the smallest things, like whitespace changes.
Developers often update stored snapshots when they see them fail without thinking too much about why they fail.

Tests should tell a story and be considered an instant red flag whenever they fail.
They should focus on the important details like the data a component is displaying
or if the data is coming from a prop or being dynamically calculated.

Instead, consider writing simple but precise assertion tests.

**Do:**
```js
const { getByText, getByRole } = render(<MyComponent />);
expect(getByText('Hello, World!')).toBeInTheDocument();
expect(getByRole('button')).toHaveTextContent('Save');
```

**Don't:**
```js
const { container } = render(<MyComponent />); // react-testing-library
expect(container).toMatchSnapshot();
```

## Avoid time-based waits

Sometimes the easiest solution to fixing a test is adding a wait/sleep call. In most cases, though,
this can't be considered a reliable fix, because:

1. It significantly increases total test run time, especially when used often
2. Every machine will take a different amount of time to execute the code, and some &mdash; especially CI runners
&mdash; are prone to lag during the test run.

Instead, use the utilities available for every testing framework to wait for elements to appear
or for asynchronous operations to finish execution.

**Do:**
```js
screen.getByRole('button', { name: 'Save document' });
expect(await screen.findByText('Document saved successfully')).toBeInTheDocument();
```

**Don't:**
```js
screen.getByRole('button', { name: 'Save document' });
await new Promise((resolve) => setTimeout(resolve, 1000));
expect(screen.getByText('Document saved successfully')).toBeInTheDocument();
```

## Write clear test names

Test cases and suites should have unambiguous names and match the naming convention throughout the project.
Use short but descriptive names written in plain English.

We recommend using the third-person singular simple present tense for its short form and ease of reading.

**Do:**
```js
describe('arraySearch()', () => { // use tested function name as the root group name
it('accepts object arrays', () => { /* [...] */ });
it('accepts string arrays', () => { /* [...] */ });
it('throw on non-array inputs', () => { /* [...] */});
it('supports the `options.caseSensitive` option', () => { /*[...]*/ });
});
```

**Don't:**
```js
describe('array search', () => { // bad: not pointing to what exactly this group is testing
it('object arrays', () => { /* [...] */ }); // bad: not enough context
it('arraySearch(["a", "b"])', () => { /* [...] */ }); // bad: function call example may not be easily understandable
it('should throw on non-array inputs', () => { /* [...] */ });
it('supports options.caseSensitive', () => { /* [...] */ }); // bad: using two different naming conventions; see line above
});
```

### Wrap property names and data in `` ` ``

When including property and argument names in the test name string, wrap them in backticks (`` ` ``) to clearly
separate them from the rest of the text.

**Do:**
```js
it('returns an empty object when the `value` string is empty');
```

**Don't:**
```js
it('returns an empty object when the value string is empty');
```

## Add debug-friendly comments or error messages

Consider adding custom error messages or code comments for assertions that are not obvious.
For [jest](https://jestjs.io/), we recommend adding a comment on top or to the right of the assertion,
so in case of an error, it will be printed out together as the failing code fragment.

**Do:**
```js
// Total should equal the result of 1234^2 / 3.14 rounded to two decimal places
expect(screen.getByText('Total: 484954.14')).toBeInTheDocument();
```
1 change: 1 addition & 0 deletions website/docs/02_components/_category_.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
label: Components
collapsed: false
position: 1
3 changes: 3 additions & 0 deletions website/docs/02_components/display/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
label: Display
collapsed: false
position: 4
3 changes: 3 additions & 0 deletions website/docs/02_components/display/callout/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
link:
type: doc
id: component_callout_overview
6 changes: 6 additions & 0 deletions website/docs/02_components/display/callout/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Callout
id: component_callout_overview
export_name: EuiCallOut
slug: /components/callout
---
35 changes: 35 additions & 0 deletions website/docs/02_components/display/callout/testing.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: Testing EuiCallOut
sidebar_label: Testing
---

import QuickReference from './testing_quick_reference.svg';

## Quick reference

<QuickReference />

## How to test EuiCallOut?

**EuiCallOut** adds simple wrapper elements to the rendered title, icon, and children. When testing, you should
focus on the inner content that's being passed to the component instead of testing the exact DOM structure.

We recommend testing for the expected text to exist in the document or within a section of the document it's supposed
to be rendered in (e.g., by `screen.getByText('Lorem ipsum')`). In case there might be multiple EuiCallOuts rendered,
we recommend adding `data-test-subj` attributes to each of them and running queries within specific EuiCallOut
elements to ensure the test is running assertions on the right one.

## Testing icon type

You can use the `data-icon-type` attribute of the `.euiIcon` element to check what icon type is rendered.
This level of detail in tests is often unnecessary, and we recommend having really good tests
for the inner data this component renders first before testing icon and callout types.

## Available selectors

| Selector | Description |
|:---------------------------------------------|:--------------------|
| `.euiCallOut` | Root element |
| `.euiCallOutHeader__title` | Callout title |
| `.euiIcon` | Callout header icon |
| `[data-test-subj="euiDismissCalloutButton"]` | Dismiss button |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions website/docs/02_components/display/text/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
link:
type: doc
id: component_text_overview
6 changes: 6 additions & 0 deletions website/docs/02_components/display/text/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
id: component_text_overview
title: Text
export_name: EuiText
slug: /components/text
---
15 changes: 15 additions & 0 deletions website/docs/02_components/display/text/testing.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: Testing EuiText
sidebar_label: Testing
---

:::warning
In most cases, testing for the usage of `<EuiText>` brings very little value and **should be omitted**.
Instead of testing the implementation details, focus on testing if the contents rendered inside are correct.
:::

## Available selectors

| Selector | Description |
|:-----------|:-------------|
| `.euiText` | Root element |
3 changes: 3 additions & 0 deletions website/docs/02_components/display/title/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
link:
type: doc
id: component_title_overview
6 changes: 6 additions & 0 deletions website/docs/02_components/display/title/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
id: component_title_overview
title: Title
export_name: EuiTitle
slug: /components/title
---
15 changes: 15 additions & 0 deletions website/docs/02_components/display/title/testing.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: Testing EuiTitle
sidebar_label: Testing
---

:::warning
In most cases, testing for the usage of `<EuiTitle>` brings very little value and **should be omitted**.
Instead of testing the implementation details, focus on testing if the contents rendered inside are correct.
:::

## Available selectors

| Selector | Description |
|:------------|:-------------|
| `.euiTitle` | Root element |
2 changes: 1 addition & 1 deletion website/docs/02_components/layout/_category_.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
label: Layout
collapsed: false
position: 1
position: 2
2 changes: 1 addition & 1 deletion website/docs/02_components/navigation/_category_.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
label: Navigation
collapsed: false
position: 2
position: 3
37 changes: 37 additions & 0 deletions website/docs/02_components/navigation/button/testing.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: Testing EuiButton and EuiButtonIcon
sidebar_label: Testing
---

`<EuiButton>`, `<EuiButtonIcon>` and `<EuiButtonEmpty>` are all rendering native `<button>` or `<a>` elements.

## Buttons as `a` elements

When `<EuiButton>`, `<EuiButtonIcon>` or `<EuiButtonEmpty>` receive a valid `href` property, they will render
an anchor (`<a>`) element instead of a button (`<button>`). Please keep in mind that no matter what element
is used underneath, they'll look the same, so it's best to open Dev Tools and check what element to query.

## Icons

In order to confirm the right icon is rendered, you can follow the same steps as you'd do for `<EuiIcon>`.
You should check if the `<button>` (or `<a>` - [see above](#buttons-as-a-elements)) element has a `<svg>` element inside to ensure
an icon is being displayed, and compare the `data-icon-type` attribute value.
It matches the type of icon being displayed on the screen.

## Content (children)

Button content is wrapped in multiple inner elements to ensure correct styles, however, we don't recommend querying
using the inner structure of our components. Instead, you should **read the inner text** of the rendered `<button>`
(or `<a>` - [see above](#buttons-as-a-elements)) and confirm it's what you expected it to be.

You can do that by reading the [`innerText`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText)
or [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) properties of the element.
Read about the differences between these two
[here](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#differences_from_innertext).

## Available selectors

| Selector | Description |
|:----------------|:-----------------------------------------------------------------------|
| `button` or `a` | Root button element |
| `svg` | Button icon<br />Use `data-icon-type` to verify the right icon is used |
Loading

0 comments on commit 5ff0212

Please sign in to comment.