Skip to content

Commit

Permalink
Install React Testing Library and export several RTL test helpers (#6091
Browse files Browse the repository at this point in the history
)

* Add RTL dependencies (at the same versions that Kibana uses)

* Add basic EuiPopover RTL tests

* consolidate react types yarn.lock entries

* Add custom RTL queries and render/screen

* consolidate more yarn.lock entries

* Fix custom RTL render ignoring testenv mocks

* Add RTL helpers for waiting for an EuiPopover to open/close

* [Misc] Tweak typing by using correct get (vs query/find) API

* Move RTL helpers to `src/test/rtl` instead of being exported by `src/test/`

- Since RTL is a devDependency not a peerDependency, we can't assume consumers will have the library installed, and need to make reaching for it optional

* Add Typescript definitions for helpers

* Add unit tests for new generic test helpers

+ helps with quick type checking as well

* Changelog/consuming documentation

Co-authored-by: Greg Thompson <thompson.glowe@gmail.com>
Co-authored-by: Chandler Prall <chandler.prall@gmail.com>
  • Loading branch information
3 people authored Aug 2, 2022
1 parent a8805f4 commit ca34f24
Show file tree
Hide file tree
Showing 20 changed files with 752 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ es
lib
test-env
types
eui.d.ts
**/*.d.ts
package.json
docs
packages
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
"@svgr/core": "5.5.0",
"@svgr/plugin-svgo": "^4.0.3",
"@testing-library/dom": "^8.12.0",
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "^13.5.0",
"@types/classnames": "^2.2.10",
"@types/enzyme": "^3.10.5",
"@types/jest": "^24.0.6",
Expand All @@ -135,6 +140,7 @@
"@types/react-is": "^17.0.3",
"@types/react-router-dom": "^5.1.5",
"@types/tabbable": "^3.1.2",
"@types/testing-library__jest-dom": "^5.14.3",
"@types/url-parse": "^1.4.8",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^5.10.2",
Expand Down
11 changes: 11 additions & 0 deletions scripts/compile-eui.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ function compileBundle() {
return null;
}
});

// dtsGenerator is unfortunately having massive issues with RTL type defs, so we're
// temporarily defining manual `.d.ts` files and copying them to each compiled dir
shell.mkdir('-p', `${dir}/rtl`);
glob('./src/test/rtl/**/*.d.ts', undefined, (error, files) => {
files.forEach(file => {
const splitPath = file.split('/');
const fileName = splitPath[splitPath.length - 1];
shell.cp('-f', `${file}`, `${dir}/rtl/${fileName}`);
});
});
})
console.log(chalk.green('✔ Finished test utils files'));

Expand Down
4 changes: 2 additions & 2 deletions scripts/jest/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
},
"setupFiles": [
"<rootDir>/scripts/jest/setup/enzyme.js",
"<rootDir>/scripts/jest/setup/throw_on_console_error.js"
"<rootDir>/scripts/jest/setup/throw_on_console_error.js",
"<rootDir>/scripts/jest/setup/mocks.js"
],
"setupFilesAfterEnv": [
"<rootDir>/scripts/jest/setup/mocks.js",
"<rootDir>/scripts/jest/setup/polyfills.js",
"<rootDir>/scripts/jest/setup/unmount_enzyme.js"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,12 @@ import { Timer } from '@elastic/eui/lib/services/time';`}
</EuiText>
<EuiSpacer />
<EuiCodeBlock language="jsx" isCopyable fontSize="m">
{"import { findTestSubject } from '@elastic/eui/lib/test';"}
{
"import { findTestSubject } from '@elastic/eui/lib/test'; // Enzyme"
}
{
"import { findByTestSubject, render, screen } from '@elastic/eui/lib/test/rtl'; // React Testing Library"
}
</EuiCodeBlock>
</>
),
Expand Down
86 changes: 86 additions & 0 deletions src/components/popover/__snapshots__/popover.rtl.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`EuiPopover snapshot testing renders 1`] = `
<body>
<div>
<div
class="euiPopover testClass1 testClass2 emotion-euiPopover"
data-test-subj="test subject string"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<button />
</div>
</div>
</div>
</body>
`;

exports[`EuiPopover snapshot testing renders with popover contents 1`] = `
<body>
<div>
<div
class="euiPopover euiPopover-isOpen testClass1 testClass2 emotion-euiPopover"
data-test-subj="test subject string"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<button />
</div>
</div>
</div>
<div
data-euiportal="true"
>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="-1"
/>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="-1"
/>
<div
data-focus-lock-disabled="disabled"
>
<div
aria-describedby="generated-id"
aria-label="aria-label"
aria-live="off"
aria-modal="true"
class="euiPanel euiPanel--plain euiPanel--paddingMedium euiPopover__panel emotion-euiPanel-grow-m-m-plain-euiPopover__panel-isOpen-bottom"
data-autofocus="true"
data-popover-open="true"
data-popover-panel="true"
role="dialog"
style="top: 16px; left: -22px; will-change: transform, opacity; z-index: 2000;"
tabindex="0"
>
<div
class="emotion-euiPopoverArrow-bottom"
data-popover-arrow="bottom"
style="left: 10px; top: 0px;"
/>
<p
class="emotion-euiScreenReaderOnly"
id="generated-id"
>
You are in a dialog. To close this dialog, hit escape.
</p>
<div>
Hello world
</div>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="-1"
/>
</div>
</body>
`;
105 changes: 105 additions & 0 deletions src/components/popover/popover.rtl.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React, { useState } from 'react';
import { fireEvent } from '@testing-library/react';
import {
render,
screen,
waitForEuiPopoverOpen,
waitForEuiPopoverClose,
} from '../../test/rtl';
import { requiredProps } from '../../test';

import { EuiPopover } from './';

describe('EuiPopover', () => {
describe('snapshot testing', () => {
it('renders', () => {
const { baseElement } = render(
<EuiPopover
button={<button />}
closePopover={() => {}}
{...requiredProps}
/>
);

// NOTE: Using baseElement instead of container is required for components that use portals
expect(baseElement).toMatchSnapshot();
});

it('renders with popover contents', () => {
const { baseElement } = render(
<EuiPopover
button={<button />}
closePopover={() => {}}
isOpen={true}
{...requiredProps}
>
Hello world
</EuiPopover>
);

expect(baseElement).toMatchSnapshot();
});
});

const mockPopoverInteraction = jest.fn();
const MockPopoverComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const togglePopover = () => setIsOpen(!isOpen);
const closePopover = () => setIsOpen(false);

return (
<EuiPopover
button={<button onClick={togglePopover}>Open popover</button>}
closePopover={closePopover}
isOpen={isOpen}
data-test-subj="popover"
>
<span data-test-subj="fff">Popover content</span>
<button onClick={mockPopoverInteraction}>Button inside popover</button>
</EuiPopover>
);
};

describe('open/close behavior', () => {
it('opens the popover, contents', () => {
render(<MockPopoverComponent />);

expect(screen.queryByText('Popover content')).toBeFalsy();

fireEvent.click(screen.getByText('Open popover'));

expect(screen.queryByText('Popover content')).toBeTruthy();
});

it('allows interacting with popover children', async () => {
render(<MockPopoverComponent />);

fireEvent.click(screen.getByText('Open popover'));
await waitForEuiPopoverOpen();

fireEvent.click(screen.getByText('Button inside popover'));
expect(mockPopoverInteraction).toHaveBeenCalledTimes(1);
});

it('closes the popover on escape key press', async () => {
render(<MockPopoverComponent />);

fireEvent.click(screen.getByText('Open popover'));
await waitForEuiPopoverOpen();

fireEvent.keyDown(screen.getByTestSubject('popover'), {
key: 'Escape',
});

await waitForEuiPopoverClose();
});
});
});
7 changes: 7 additions & 0 deletions src/test/rtl/component_helpers.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Ensure the EuiPopover being tested is open/closed before contiuning
* Note: Because EuiPopover is portalled, we want to query `document`
* instead of the `container` returned by RTL's render()
*/
export declare const waitForEuiPopoverOpen: () => Promise<void>;
export declare const waitForEuiPopoverClose: () => Promise<void>;
26 changes: 26 additions & 0 deletions src/test/rtl/component_helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { waitFor } from '@testing-library/react';

/**
* Ensure the EuiPopover being tested is open/closed before contiuning
* Note: Because EuiPopover is portalled, we want to query `document`
* instead of the `container` returned by RTL's render()
*/
export const waitForEuiPopoverOpen = async () =>
await waitFor(() => {
const openPopover = document.querySelector('[data-popover-open]');
expect(openPopover).toBeTruthy();
});

export const waitForEuiPopoverClose = async () =>
await waitFor(() => {
const openPopover = document.querySelector('[data-popover-open]');
expect(openPopover).toBeFalsy();
});
74 changes: 74 additions & 0 deletions src/test/rtl/custom_render.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ReactElement } from 'react';
import { queries, RenderOptions, Screen } from '@testing-library/react';
import * as dataTestSubjQueries from './data_test_subj_queries';
/**
* Custom render() fn with EuiProvider and query helpers
*
* @see https://testing-library.com/docs/react-testing-library/setup#custom-render
* @see https://testing-library.com/docs/react-testing-library/setup#add-custom-queries
*/
declare const customRender: (ui: ReactElement, { queries: renderQueries, ...options }?: RenderOptions) => import("@testing-library/react").RenderResult<{
getByLabelText: typeof queries.getByLabelText;
getAllByLabelText: typeof queries.getAllByLabelText;
queryByLabelText: typeof queries.queryByLabelText;
queryAllByLabelText: typeof queries.queryAllByLabelText;
findByLabelText: typeof queries.findByLabelText;
findAllByLabelText: typeof queries.findAllByLabelText;
getByPlaceholderText: typeof queries.getByPlaceholderText;
getAllByPlaceholderText: typeof queries.getAllByPlaceholderText;
queryByPlaceholderText: typeof queries.queryByPlaceholderText;
queryAllByPlaceholderText: typeof queries.queryAllByPlaceholderText;
findByPlaceholderText: typeof queries.findByPlaceholderText;
findAllByPlaceholderText: typeof queries.findAllByPlaceholderText;
getByText: typeof queries.getByText;
getAllByText: typeof queries.getAllByText;
queryByText: typeof queries.queryByText;
queryAllByText: typeof queries.queryAllByText;
findByText: typeof queries.findByText;
findAllByText: typeof queries.findAllByText;
getByAltText: typeof queries.getByAltText;
getAllByAltText: typeof queries.getAllByAltText;
queryByAltText: typeof queries.queryByAltText;
queryAllByAltText: typeof queries.queryAllByAltText;
findByAltText: typeof queries.findByAltText;
findAllByAltText: typeof queries.findAllByAltText;
getByTitle: typeof queries.getByTitle;
getAllByTitle: typeof queries.getAllByTitle;
queryByTitle: typeof queries.queryByTitle;
queryAllByTitle: typeof queries.queryAllByTitle;
findByTitle: typeof queries.findByTitle;
findAllByTitle: typeof queries.findAllByTitle;
getByDisplayValue: typeof queries.getByDisplayValue;
getAllByDisplayValue: typeof queries.getAllByDisplayValue;
queryByDisplayValue: typeof queries.queryByDisplayValue;
queryAllByDisplayValue: typeof queries.queryAllByDisplayValue;
findByDisplayValue: typeof queries.findByDisplayValue;
findAllByDisplayValue: typeof queries.findAllByDisplayValue;
getByRole: typeof queries.getByRole;
getAllByRole: typeof queries.getAllByRole;
queryByRole: typeof queries.queryByRole;
queryAllByRole: typeof queries.queryAllByRole;
findByRole: typeof queries.findByRole;
findAllByRole: typeof queries.findAllByRole;
getByTestId: typeof queries.getByTestId;
getAllByTestId: typeof queries.getAllByTestId;
queryByTestId: typeof queries.queryByTestId;
queryAllByTestId: typeof queries.queryAllByTestId;
findByTestId: typeof queries.findByTestId;
findAllByTestId: typeof queries.findAllByTestId;
queryByTestSubject: import("@testing-library/react").QueryBy<[import("@testing-library/react").Matcher]>;
queryAllByTestSubject: (container: HTMLElement, id: import("@testing-library/react").Matcher, options?: import("@testing-library/react").MatcherOptions | undefined) => HTMLElement[];
getByTestSubject: import("@testing-library/react").GetBy<[import("@testing-library/react").Matcher]>;
getAllByTestSubject: import("@testing-library/react").GetAllBy<[import("@testing-library/react").Matcher]>;
findAllByTestSubject: import("@testing-library/react").FindAllBy<[import("@testing-library/react").Matcher]>;
findByTestSubject: import("@testing-library/react").FindBy<[import("@testing-library/react").Matcher]>;
}, HTMLElement, HTMLElement>;
export { customRender as render };
/**
* Custom screen util with EUI query helpers
*
* @see https://testing-library.com/docs/queries/about/#screen
* @see https://github.com/testing-library/dom-testing-library/issues/516
*/
declare const customScreen: Screen<typeof queries & typeof dataTestSubjQueries>;
export { customScreen as screen };
Loading

0 comments on commit ca34f24

Please sign in to comment.