Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add utilities for mounting and un-mounting components #53

Merged
merged 1 commit into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
This package contains common utilities for testing UI components across
Hypothesis frontend projects. It includes tools for:

- Rendering UI components and unmounting them once the test ends
- Waiting for conditions to be met
- Mocking UI components
- Testing accessibility using [axe-core](https://github.com/dequelabs/axe-core)
Expand All @@ -14,3 +15,36 @@ standard UI and UI testing stack, built on:
- [Enzyme](https://github.com/enzymejs/enzyme)
- [babel-plugin-mockable-imports](https://github.com/robertknight/babel-plugin-mockable-imports)

## API guide

### Rendering components

This package exports a wrapper around Enzyme's `mount` function to render
a component, query its output and interact with it. The function in this
package adds the wrapper to a global list of active wrappers which can then
be conveniently unmounted using `unmountAll` at the end of a test.

```js
import { mount, unmountAll } from '@hypothesis/frontend-testing';

describe('MyWidget', () => {
afterEach(() => {
// Clean up by unmounting any wrappers mounted in the current test and
// removing associated DOM containers.
unmountAll();
});

it('should render', () => {
const wrapper = mount(<MyWidget/>);

// Query component content etc.
});

it('should do something that requires component to be connected', () => {
const wrapper = mount(<MyWidget/>, { connected: true });

// Test behavior that relies on rendered component being part of the
// DOM tree under `document.body`.
});
});
```
5 changes: 3 additions & 2 deletions src/enzyme.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
declare module 'enzyme' {
export class ReactWrapper {
getDOMNode(): HTMLElement;
unmount(): void;
}

export function mount(
elementOrWrapper: VNode | ReactWrapper,
{ attachTo: HTMLElement },
);
options?: { attachTo?: HTMLElement },
): ReactWrapper;
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export type { Scenario } from './accessibility.js';
export { checkAccessibility } from './accessibility.js';
export { mockImportedComponents } from './mock-imported-components.js';
export { mount, unmountAll } from './mount.js';
export type { MountOptions } from './mount.js';
export type { TestTimeout, TimeoutSpec } from './wait.js';
export { delay, waitFor, waitForElement } from './wait.js';
48 changes: 48 additions & 0 deletions src/mount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as enzyme from 'enzyme';
import type { ReactWrapper } from 'enzyme';
import type { VNode } from 'preact';

let containers: HTMLElement[] = [];
let wrappers: ReactWrapper[] = [];

export type MountOptions = {
/**
* If true, the element will be rendered in a container element which is
* attached to `document.body`.
*/
connected?: boolean;
};
Comment on lines +8 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is convenient, as very frequently we need the element to actually be in the DOM and we don't care about the container, but there are cases in which we need to customize the container with styles and such. See this test for example.

Maybe we could allow an HTMLElement to be conditionally provided, either by allowing the connected option to be connected?: boolean | HTMLElement, or by providing a more complex union type where either connected?: boolean or container?: HTMLElement to be allowed.

The later is probably more correct from a type point of view, but will require more checks.

We could also try to find a different option name where boolean | HTMLElement feels less awkward.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To begin with I would suggest that callers continue to use the underlying Enzyme API if they need to customize the container. When we better understand how custom containers are used then we could integrate it into this convenience API. There are some details I'm not sure about. For example, with this API the container is "owned" by this package and gets removed when unmountAll is called. I'm not sure if that is appropriate for custom containers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good point.

Let's land this. It will allow us to remove a lot of duplicated code already.


/**
* Render a Preact component using Enzyme and return a wrapper.
*
* The component can be unmounted by calling `wrapper.unmount()` or by calling
* {@link unmountAll} at the end of the test.
*/
export function mount(jsx: VNode, { connected = false }: MountOptions = {}) {
let wrapper;
if (connected) {
const container = document.createElement('div');
container.setAttribute('data-enzyme-container', '');
containers.push(container);
wrapper = enzyme.mount(jsx, { attachTo: container });
} else {
wrapper = enzyme.mount(jsx);
}

wrappers.push(wrapper);

return wrapper;
}

/**
* Unmount all Preact components rendered using {@link mount} and remove their
* parent container elements (if any) from the DOM.
*/
export function unmountAll() {
wrappers.forEach(w => w.unmount());
wrappers = [];

containers.forEach(c => c.remove());
containers = [];
}