Skip to content

Commit

Permalink
Fix and improve feature tests
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboc committed Jun 8, 2022
1 parent 788ceb9 commit 68618cc
Show file tree
Hide file tree
Showing 28 changed files with 906 additions and 990 deletions.
3 changes: 3 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Enforce Node and pnpm versions as specified in `package.json`
engine-strict=true

# Save exact dependency versions in `package.json`
save-prefix=""

Expand Down
112 changes: 112 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
- [Fixing and formatting](#fixing-and-formatting)
- [Editor integration](#editor-integration)
- [Testing](#testing)
- [Feature tests](#feature-tests)
- [Fake timers](#fake-timers)
- [Debugging](#debugging)
- [Visual regression](#visual-regression)
- [Deployment](#deployment)
- [Release process](#release-process)
Expand Down Expand Up @@ -204,6 +207,115 @@ install the recommended extensions.
> located in `jest.config.json`. This results in a nicer terminal output when
> running tests on the entire workspace.
### Feature tests

The `@h5web/app` package includes feature tests written with
[React Testing Library](https://testing-library.com/docs/react-testing-library/intro).
They are located under `src/__tests__`. Each file covers a particular subtree of
components of H5Web.

H5Web's feature tests typically consist in rendering the entire app with mock
data (i.e. inside `MockProvider`), executing an action like a real user would
(e.g. clicking on a button, pressing a key, etc.), and then expecting something
to happen in the DOM as a result. Most tests, perform multiple actions and
expectations consecutively to minimise the overhead of rendering the entire app
again and again.

`MockProvider` resolves most requests instantaneously to save time in tests, but
its API's methods are still called asynchronously like other providers. This
means that during tests, `Suspense` loading fallbacks render just like they
would normally; they just don't stick around in the DOM for long.

This adds a bit of complexity when testing, as React doesn't like when something
happens after a test has completed. In fact, we have to ensure that every
component that suspends inside a test **finishes loading before the end of that
test**. This is where Testing Library's
[asynchronous methods](https://testing-library.com/docs/dom-testing-library/api-async)
come in.

To keep tests readable and focused, H5Web's testing utilities `renderApp` and
`selectExplorerNode` call Testing Library's `waitFor` utility for you to wait
for suspended components to finish loading (cf. `waitForAllLoaders` in
`test-utils.tsx`). As a result, in most cases, you can just use synchronous
queries like `getBy` in your tests.

#### Fake timers

To allow developing and testing loading interfaces, as well as features like
cancel/retry, `MockProvider` adds an artificial delay of 3s (`SLOW_TIMEOUT`) to
some requests, notably to value requests for datasets prefixed with `slow_`.

In order for this artificial delay to not slow down feature tests, we must use
[fake timers](https://testing-library.com/docs/using-fake-timers/). This is done
by setting the `withFakeTimers` option when calling `renderApp()`:

```ts
renderApp({ withFakeTimers: true });
```

When `withFakeTimers` is set, `renderApp` and `selectExplorerNode` no longer
wait for suspended components to finish loading. It is up to you to wait for and
expect the loaders you're interested in testing to appear, and then to wait for
and expect their corresponding suspended components to replace them on the
screen. You can do so with Testing Library's async methods: `findBy` or
`waitFor`.

Indeed, when Jest's fake timers are enabled, `findBy` and `waitFor` use
`jest.advanceTimersByTime()` internally to simulate the passing of time. By
default, they advance the clock by intervals of
[50 ms](https://github.com/testing-library/dom-testing-library/blob/main/src/wait-for.js#L25)
and give up after
[1000 ms](https://github.com/testing-library/dom-testing-library/blob/main/src/config.ts#L14)
(20 attempts). Therefore, the easiest way to skip the artificial delay
introduced by `MockProvider` is to configure the `waitFor` timeout:

```ts
import { SLOW_TIMEOUT } from '../providers/mock/mock-api';
import { renderApp } from '../test-utils';

test('show loader while fetching dataset value', async () => {
await renderApp({
initialPath: '/resilience/slow_value',
withFakeTimers: true,
});

// Wait for value loader to appear
await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

// Wait for all slow fetches to succeed (i.e. for visualization to appear)
await expect(
screen.findByText(/42/, undefined, { timeout: SLOW_TIMEOUT })
).resolves.toBeVisible();
});
```

> It's important _not_ to use Jest's `advanceTimersByTime` directly to avoid
> [`act()` warnings](https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning#the-dreaded-act-warning).
Note that when using fake timers, you may also need to use `findBy` and
`waitFor` after triggering an action. For instance:

```ts
await user.click(screen.getByRole('button', { name: /Retry/ }));
await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();
```

#### Debugging

You can use Testing Library's
[`prettyDOM` utility](https://testing-library.com/docs/dom-testing-library/api-debugging#prettydom)
to log the state of the DOM anywhere in your tests:

```ts
console.debug(prettyDOM()); // if you use `console.log` without mocking it, the test will fail
console.debug(prettyDOM(screen.getByText('foo'))); // you can also print out a specific element
```

To ensure that the entire DOM is printed out in the terminal, you may have to
set environment variable `DEBUG_PRINT_LIMIT`
[to a large value](https://testing-library.com/docs/dom-testing-library/api-debugging#automatic-logging)
when calling `pnpm test`.

### Visual regression

Cypress is used for end-to-end testing but also for visual regression testing.
Expand Down
6 changes: 2 additions & 4 deletions packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { ErrorBoundary } from 'react-error-boundary';
import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex';

import styles from './App.module.css';
import EntityLoader from './EntityLoader';
import ErrorFallback from './ErrorFallback';
import LoadingFallback from './LoadingFallback';
import VisConfigProvider from './VisConfigProvider';
import BreadcrumbsBar from './breadcrumbs/BreadcrumbsBar';
import type { FeedbackContext } from './breadcrumbs/models';
Expand Down Expand Up @@ -72,9 +72,7 @@ function App(props: Props) {
resetKeys={[selectedPath, isInspecting]}
FallbackComponent={ErrorFallback}
>
<Suspense
fallback={<LoadingFallback isInspecting={isInspecting} />}
>
<Suspense fallback={<EntityLoader isInspecting={isInspecting} />}>
{isInspecting ? (
<MetadataViewer
path={selectedPath}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,29 @@ import { useTimeout } from 'react-use';

import styles from './App.module.css';

const LOADER_DELAY = 100;

interface Props {
isInspecting: boolean;
message?: string;
}

function LoadingFallback(props: Props) {
function EntityLoader(props: Props) {
const { isInspecting, message = 'Loading' } = props;

// Wait a bit before showing loader to avoid flash
const [isReady] = useTimeout(100);
const [isReady] = useTimeout(LOADER_DELAY);

return (
<>
<div
className={styles.fallbackBar}
data-mode={isInspecting ? 'inspect' : 'display'}
data-testid="LoadingEntity" // bypass `LOADER_DELAY` in tests
/>
{isReady() && <p className={styles.fallback}>{message}...</p>}
</>
);
}

export default LoadingFallback;
export default EntityLoader;
Loading

0 comments on commit 68618cc

Please sign in to comment.