('master');
16 |
--------------------------------------------------------------------------------
/tests/shared/components/UserWithRouter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withRouter } from 'react-router';
3 | import { Link } from 'react-router-dom';
4 |
5 | function UserPage({ match: { params } }) {
6 | return (
7 |
8 | User #{params.userId} Next user
9 |
10 | );
11 | }
12 |
13 | export const UserWithRouter = withRouter(UserPage);
14 |
--------------------------------------------------------------------------------
/ui/svg/settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/render-text/test.js:
--------------------------------------------------------------------------------
1 | // highlight{8,10}
2 | import React from 'react';
3 | import { mount } from 'enzyme';
4 | import { HelloMessage } from 'shared/components/HelloMessage';
5 |
6 | it('renders personalized greeting', () => {
7 | // Render new instance in every test to prevent leaking state
8 | const wrapper = mount( );
9 |
10 | expect(wrapper.text()).toMatch(/hello Satoshi/i);
11 | });
12 |
--------------------------------------------------------------------------------
/ui/import-files.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /* eslint-env commonjs */
3 |
4 | import type { TTestKinds } from './types';
5 |
6 | // NOTE: Tests are populated at compile time via import-tests-loader, because
7 | // we're lazy (smart) and don't want to update this file whenever we add a test
8 | export const testKinds: TTestKinds = {};
9 |
10 | // NOTE: This is also replaced at compile time with the latest commit SHA
11 | export const gitRef: string = '';
12 |
--------------------------------------------------------------------------------
/ui/svg/question.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ui/components/File/Code/__fixtures__/xhr.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { createFixture } from 'react-cosmos-classic';
4 | import { Code } from '..';
5 | import { testKinds } from '../../../../import-files';
6 |
7 | export default [
8 | createFixture({
9 | component: Code,
10 | props: {
11 | code: testKinds['jest-enzyme'].tests[6].files['test.js'],
12 | showComments: true,
13 | showImports: false
14 | }
15 | })
16 | ];
17 |
--------------------------------------------------------------------------------
/ui/svg/clippy.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ui/svg/link.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tests/jest-rtl/render-text/test.js:
--------------------------------------------------------------------------------
1 | // highlight{8,10}
2 | import React from 'react';
3 | import { render, waitForElement } from 'react-testing-library';
4 | import { HelloMessage } from 'shared/components/HelloMessage';
5 |
6 | it('renders personalized greeting', async () => {
7 | // Render new instance in every test to prevent leaking state
8 | const { getByText } = render( );
9 |
10 | await waitForElement(() => getByText(/hello Satoshi/i));
11 | });
12 |
--------------------------------------------------------------------------------
/tests/shared/components/StatefulCounter.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export class StatefulCounter extends Component {
4 | state = {
5 | count: 0
6 | };
7 |
8 | increment = () => {
9 | this.setState({ count: this.state.count + 1 });
10 | };
11 |
12 | render() {
13 | return (
14 |
15 | Clicked {this.state.count} times{' '}
16 | +1
17 |
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/click-callback/test.js:
--------------------------------------------------------------------------------
1 | // highlight{9,11}
2 | import React from 'react';
3 | import { mount } from 'enzyme';
4 | import { Button } from 'shared/components/Button';
5 |
6 | it('calls "onClick" prop on button click', () => {
7 | // Render new instance in every test to prevent leaking state
8 | const onClick = jest.fn();
9 | const wrapper = mount( );
10 |
11 | wrapper.find('button').simulate('click');
12 | expect(onClick).toHaveBeenCalled();
13 | });
14 |
--------------------------------------------------------------------------------
/ui/shared/testKinds.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { testKinds } from '../import-files';
4 |
5 | import type { TTestKindId } from '../types';
6 |
7 | export const TEST_KIND_LABELS = {
8 | 'jest-enzyme': 'Enzyme',
9 | 'jest-rtl': 'react-testing-library',
10 | 'jest-interactor': '@bigtest/interactor'
11 | };
12 |
13 | export const DEFAULT_TEST_KIND_ID = 'jest-rtl';
14 |
15 | export function getTestKind(testKindId?: TTestKindId) {
16 | return testKinds[testKindId || DEFAULT_TEST_KIND_ID];
17 | }
18 |
--------------------------------------------------------------------------------
/ui/webpack-loaders/import-tests-loader.test.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import loader from './import-tests-loader';
4 |
5 | const input = `
6 | export const testKinds: TTestKinds = {};
7 | export const gitRef: string = '';
8 | `;
9 | const output = loader.call({ addContextDependency: jest.fn() }, input);
10 |
11 | it('embeds tests', () => {
12 | expect(output).toMatch('jest-enzyme');
13 | expect(output).toMatch('jest-rtl');
14 | expect(output).toMatch('render-text');
15 | expect(output).toMatch('localstorage');
16 | });
17 |
--------------------------------------------------------------------------------
/tests/jest-rtl/click-callback/test.js:
--------------------------------------------------------------------------------
1 | // highlight{9,11}
2 | import React from 'react';
3 | import { render, fireEvent } from 'react-testing-library';
4 | import { Button } from 'shared/components/Button';
5 |
6 | it('calls "onClick" prop on button click', () => {
7 | // Render new instance in every test to prevent leaking state
8 | const onClick = jest.fn();
9 | const { getByText } = render( );
10 |
11 | fireEvent.click(getByText(/click me nao/i));
12 | expect(onClick).toHaveBeenCalled();
13 | });
14 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/local-state/README.md:
--------------------------------------------------------------------------------
1 | ## Local component state
2 |
3 | The component reads and updates a counter from its [local state](https://reactjs.org/docs/state-and-lifecycle.html#adding-local-state-to-a-class).
4 |
5 | We test that the component renders the counter value. Then we click on the increment button, which updates the local state, and afterwards test that the component renders the incremented value.
6 |
7 | > We use [`@react-mock/state`](https://github.com/skidding/react-mock/tree/master/packages/state) to mock the component state.
8 |
--------------------------------------------------------------------------------
/tests/jest-rtl/local-state/README.md:
--------------------------------------------------------------------------------
1 | ## Local component state
2 |
3 | The component reads and updates a counter from its [local state](https://reactjs.org/docs/state-and-lifecycle.html#adding-local-state-to-a-class).
4 |
5 | We test that the component renders the counter value. Then we click on the increment button, which updates the local state, and afterwards test that the component renders the incremented value.
6 |
7 | > We use [`@react-mock/state`](https://github.com/skidding/react-mock/tree/master/packages/state) to mock the component state.
8 |
--------------------------------------------------------------------------------
/tests/jest-rtl/localstorage/README.md:
--------------------------------------------------------------------------------
1 | ## LocalStorage read and write
2 |
3 | The component reads and updates a value from [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage).
4 |
5 | We test that the component renders the mocked value from LocalStorage. Then we type a new name into an input, submit the form, and test that the submitted value has been updated in LocalStorage.
6 |
7 | > We use [`@react-mock/localstorage`](https://github.com/skidding/react-mock/tree/master/packages/localstorage) to mock the cached data.
8 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/localstorage/README.md:
--------------------------------------------------------------------------------
1 | ## LocalStorage read and write
2 |
3 | The component reads and updates a value from [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage).
4 |
5 | We test that the component renders the mocked value from LocalStorage. Then we type a new name into an input, submit the form, and test that the submitted value has been updated in LocalStorage.
6 |
7 | > We use [`@react-mock/localstorage`](https://github.com/skidding/react-mock/tree/master/packages/localstorage) to mock the cached data.
8 |
--------------------------------------------------------------------------------
/tests/jest-interactor/local-state/README.md:
--------------------------------------------------------------------------------
1 | ## Local component state
2 |
3 | The component reads and updates a counter from its [local state](https://reactjs.org/docs/state-and-lifecycle.html#adding-local-state-to-a-class).
4 |
5 | We test that the component renders the counter value. Then we click on the increment button, which updates the local state, and afterwards test that the component renders the incremented value.
6 |
7 | > We use [`@react-mock/state`](https://github.com/skidding/react-mock/tree/master/packages/state) to mock the component state.
8 |
--------------------------------------------------------------------------------
/tests/jest-interactor/localstorage/README.md:
--------------------------------------------------------------------------------
1 | ## LocalStorage read and write
2 |
3 | The component reads and updates a value from [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage).
4 |
5 | We test that the component renders the mocked value from LocalStorage. Then we type a new name into an input, submit the form, and test that the submitted value has been updated in LocalStorage.
6 |
7 | > We use [`@react-mock/localstorage`](https://github.com/skidding/react-mock/tree/master/packages/localstorage) to mock the cached data.
8 |
--------------------------------------------------------------------------------
/tests/jest-interactor/render-text/test.js:
--------------------------------------------------------------------------------
1 | // highlight{7,11,13}
2 | import React from 'react';
3 | import { mount } from '@bigtest/react';
4 | import Interactor from '@bigtest/interactor';
5 | import { HelloMessage } from 'shared/components/HelloMessage';
6 |
7 | let message = new Interactor();
8 |
9 | it('renders personalized greeting', async () => {
10 | // Render new instance in every test to prevent leaking state
11 | await mount(() => );
12 |
13 | await message.when(() => expect(message.text).toEqual('Hello Satoshi'));
14 | });
15 |
--------------------------------------------------------------------------------
/tests/shared/components/ReduxCounter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | function Counter({ count, increment }) {
5 | return (
6 |
7 | Clicked {count} times +1
8 |
9 | );
10 | }
11 |
12 | function mapStateToProps({ count }) {
13 | return { count };
14 | }
15 |
16 | const mapDispatchToProps = {
17 | increment: () => ({ type: 'INCREMENT' })
18 | };
19 |
20 | export const ReduxCounter = connect(mapStateToProps, mapDispatchToProps)(
21 | Counter
22 | );
23 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 |
3 | [include]
4 |
5 | [libs]
6 | flow-typed
7 |
8 | [lints]
9 |
10 | [options]
11 | module.name_mapper='^!raw-loader!.+$' -> '/.stubs/raw.js'
12 | module.name_mapper='^!.+\/readme-text-loader!.+$' -> '/.stubs/readme-text.js'
13 | module.name_mapper='^.+\/README\.md$' -> '/.stubs/component.js'
14 | module.name_mapper='^.+\/SETUP\.md$' -> '/.stubs/component.js'
15 | module.name_mapper='^.+\/WHATSTHIS\.md$' -> '/.stubs/component.js'
16 | module.name_mapper='^.+\/CREDITS\.md$' -> '/.stubs/component.js'
17 |
18 | [strict]
19 |
--------------------------------------------------------------------------------
/CREDITS.md:
--------------------------------------------------------------------------------
1 | Tests powered by [Jest](https://jestjs.io/) [react-mock](https://github.com/skidding/react-mock) [Enzyme](https://airbnb.io/enzyme/) [react-testing-library](https://github.com/kentcdodds/react-testing-library) and [@bigtest/interactor](https://github.com/bigtestjs/interactor). Website powered by [Babel](https://babeljs.io/) [Cosmos](https://github.com/react-cosmos/react-cosmos) [MDX](https://mdxjs.com/) [Next.js](https://nextjs.org/) [Prism](https://prismjs.com/) [styled-components](https://www.styled-components.com/) [webpack](https://webpack.js.org/) and many more.
2 |
3 | Finally, [React](https://reactjs.org/) makes it all possible!
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Testing Examples
2 |
3 | [ ](https://circleci.com/gh/skidding/react-testing-examples)
4 |
5 | Searchable library of React testing examples
6 |
7 | - **[Open the library](https://react-testing-examples.netlify.app)**
8 | - [Read about the project](https://react-testing-examples.netlify.app/about)
9 | - [Browse test files](tests)
10 | - [Read contributing guide](CONTRIBUTING.md)
11 |
12 | [ ](https://react-testing-examples.netlify.app)
13 |
--------------------------------------------------------------------------------
/ui/svg/info.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ui/types.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import type { ComponentType } from 'react';
4 |
5 | export type TTestKindId = string;
6 |
7 | export type TReadmeMeta = {
8 | title: string,
9 | body: Array
10 | };
11 |
12 | export type TReadme = {
13 | meta: TReadmeMeta,
14 | component: ComponentType<*>
15 | };
16 |
17 | export type TSection = {
18 | name: string,
19 | readme: TReadme,
20 | files: { [string]: string }
21 | };
22 |
23 | export type TTestKind = {
24 | id: TTestKindId,
25 | order: string[],
26 | setup: TSection,
27 | tests: TSection[]
28 | };
29 |
30 | export type TTestKinds = {
31 | [id: TTestKindId]: TTestKind
32 | };
33 |
--------------------------------------------------------------------------------
/ui/server/testFiles.js:
--------------------------------------------------------------------------------
1 | // XXX: File uses CJS exports to be require-able by Next.js config
2 | const { join } = require('path');
3 | const glob = require('glob');
4 |
5 | const TESTS_PATH = join(__dirname, `../../tests`);
6 |
7 | exports.getTestKindIds = function() {
8 | return getDirNames(TESTS_PATH).filter(t => t !== 'shared');
9 | };
10 |
11 | exports.getTestNames = function(testKindId) {
12 | return getDirNames(join(TESTS_PATH, testKindId));
13 | };
14 |
15 | function getDirNames(dirPath) {
16 | return (
17 | glob
18 | .sync(`*/`, { cwd: dirPath })
19 | // Remove trailing slash
20 | .map(t => t.replace(/\/$/, ''))
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/ui/cosmos-proxies/globalStyle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createGlobalStyle } from 'styled-components';
3 | import { GlobalStyle } from '../global-style';
4 |
5 | export function GlobalStyleProxy({ nextProxy, ...otherProps }) {
6 | const { value: NextProxy, next } = nextProxy;
7 | const { fixture } = otherProps;
8 |
9 | return (
10 | <>
11 |
12 | {fixture.bgColor && }
13 |
14 | >
15 | );
16 | }
17 |
18 | const CustomBg = createGlobalStyle`
19 | html, body {
20 | background: ${props => props.bgColor};
21 | }
22 | `;
23 |
--------------------------------------------------------------------------------
/ui/components/SectionList/ToggleShow.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { Component } from 'react';
4 | import ReactShow from 'react-show';
5 |
6 | import type { Node } from 'react';
7 |
8 | type Props = {
9 | header: ({ show: boolean, onToggle: () => mixed }) => Node,
10 | content: Node,
11 | show: boolean,
12 | onToggle: () => mixed
13 | };
14 |
15 | export class ToggleShow extends Component {
16 | render() {
17 | const { header, content, show, onToggle } = this.props;
18 |
19 | return (
20 | <>
21 | {header({ show, onToggle })}
22 | {content}
23 | >
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ui/svg/mark-github.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ui/components/Section/__fixtures__/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { createFixture } from 'react-cosmos-classic';
4 | import { Section } from '..';
5 | import { testKinds } from '../../../import-files';
6 |
7 | export default [
8 | createFixture({
9 | name: 'Local component state',
10 | component: Section,
11 | props: {
12 | testKindId: 'jest-enzyme',
13 | section: testKinds['jest-enzyme'].tests[2],
14 | searchText: ''
15 | }
16 | }),
17 | createFixture({
18 | name: 'Fetch test',
19 | component: Section,
20 | props: {
21 | testKindId: 'jest-enzyme',
22 | section: testKinds['jest-enzyme'].tests[6],
23 | searchText: ''
24 | }
25 | })
26 | ];
27 |
--------------------------------------------------------------------------------
/tests/jest-rtl/xhr/README.md:
--------------------------------------------------------------------------------
1 | ## XHR requests
2 |
3 | The component reads and updates a server counter using the [XHR API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest).
4 |
5 | We test that the component renders the counter value from the mocked API response. Then we click on the increment button, which makes a POST request to increment the counter, and afterwards test that the component renders the incremented value.
6 |
7 | > These tests are **async** because server requests don't resolve immediately. We wait for the button to appear before interacting with our component.
8 |
9 | > We use [`@react-mock/xhr`](https://github.com/skidding/react-mock/tree/master/packages/xhr) to mock the server requests.
10 |
--------------------------------------------------------------------------------
/tests/jest-rtl/fetch/README.md:
--------------------------------------------------------------------------------
1 | ## Fetch requests
2 |
3 | The component reads and updates a server counter using the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
4 |
5 | We test that the component renders the counter value from the mocked API response. Then we click on the increment button, which makes a POST request to increment the counter, and afterwards test that the component renders the incremented value.
6 |
7 | > These tests are **async** because server requests don't resolve immediately. We wait for the button to appear before interacting with our component.
8 |
9 | > We use [`@react-mock/fetch`](https://github.com/skidding/react-mock/tree/master/packages/fetch) to mock the server requests.
10 |
--------------------------------------------------------------------------------
/ui/shared/section.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import type { TTestKindId, TSection } from '../types';
4 |
5 | type Props = {
6 | testKindId: TTestKindId,
7 | sectionName: ?string
8 | };
9 |
10 | export function hasSectionChanged(props: Props, prevProps: Props) {
11 | return (
12 | props.testKindId !== prevProps.testKindId ||
13 | props.sectionName !== prevProps.sectionName
14 | );
15 | }
16 |
17 | export function getSectionByName(
18 | sections: TSection[],
19 | sectionName: string
20 | ): TSection {
21 | const section = sections.find(s => s.name === sectionName);
22 |
23 | if (!section) {
24 | throw new Error(`Not found section with name "${sectionName}"`);
25 | }
26 |
27 | return section;
28 | }
29 |
--------------------------------------------------------------------------------
/tests/jest-interactor/xhr/README.md:
--------------------------------------------------------------------------------
1 | ## XHR requests
2 |
3 | The component reads and updates a server counter using the [XHR API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest).
4 |
5 | We test that the component renders the counter value from the mocked API response. Then we click on the increment button, which makes a POST request to increment the counter, and afterwards test that the component renders the incremented value.
6 |
7 | > These tests are **async** because server requests don't resolve immediately. We wait for the button to appear before interacting with our component.
8 |
9 | > We use [`@react-mock/xhr`](https://github.com/skidding/react-mock/tree/master/packages/xhr) to mock the server requests.
10 |
--------------------------------------------------------------------------------
/tests/jest-interactor/fetch/README.md:
--------------------------------------------------------------------------------
1 | ## Fetch requests
2 |
3 | The component reads and updates a server counter using the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
4 |
5 | We test that the component renders the counter value from the mocked API response. Then we click on the increment button, which makes a POST request to increment the counter, and afterwards test that the component renders the incremented value.
6 |
7 | > These tests are **async** because server requests don't resolve immediately. We wait for the button to appear before interacting with our component.
8 |
9 | > We use [`@react-mock/fetch`](https://github.com/skidding/react-mock/tree/master/packages/fetch) to mock the server requests.
10 |
--------------------------------------------------------------------------------
/ui/components/Header/AboutButton.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react';
4 | import Link from 'next/link';
5 | import styled from 'styled-components';
6 | import svgQuestion from '../../svg/question.svg';
7 | import { IconButton } from './styles';
8 |
9 | export function AboutButton() {
10 | return (
11 |
12 |
13 |
14 | About
15 |
16 |
17 | );
18 | }
19 |
20 | const AboutIconButton = styled(IconButton)`
21 | margin-left: 16px;
22 |
23 | @media (max-width: 399px) {
24 | .label {
25 | display: none;
26 | }
27 | `;
28 |
--------------------------------------------------------------------------------
/ui/components/Header/GithubLink.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react';
4 | import styled from 'styled-components';
5 | import svgMarkGithub from '../../svg/mark-github.svg';
6 | import { IconButton } from './styles';
7 |
8 | export function GithubLink() {
9 | return (
10 |
16 |
17 | GitHub
18 |
19 | );
20 | }
21 |
22 | const GitHubIconButton = styled(IconButton)`
23 | margin-left: 24px;
24 |
25 | @media (max-width: 359px) {
26 | .label {
27 | display: none;
28 | }
29 | }
30 | `;
31 |
--------------------------------------------------------------------------------
/ui/components/Header/styles.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import styled from 'styled-components';
4 | import { Button } from '../shared/styles';
5 |
6 | export const IconButton = styled(Button)`
7 | display: flex;
8 | height: 24px;
9 | font-weight: 400;
10 | color: #20232a;
11 | line-height: 24px;
12 | text-decoration: none;
13 |
14 | .icon {
15 | width: 24px;
16 | height: 24px;
17 | background: url(${props => props.icon});
18 | background-size: 20px;
19 | background-position: center center;
20 | background-repeat: no-repeat;
21 | opacity: 0.7;
22 | }
23 |
24 | .label {
25 | padding-left: 4px;
26 | white-space: nowrap;
27 | }
28 |
29 | :hover {
30 | .label {
31 | text-decoration: underline;
32 | }
33 | }
34 | `;
35 |
--------------------------------------------------------------------------------
/.babelrc.js:
--------------------------------------------------------------------------------
1 | const testSharedAlias = {
2 | root: ['./tests'],
3 | alias: {
4 | shared: './tests/shared'
5 | }
6 | };
7 |
8 | module.exports = {
9 | presets: ['next/babel', '@babel/preset-flow'],
10 | plugins: [
11 | ['babel-plugin-inline-import-data-uri', { extensions: ['.png', '.svg'] }],
12 | ['styled-components', { ssr: true, displayName: true, preprocess: false }],
13 | ['module-resolver', testSharedAlias],
14 | ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }]
15 | ],
16 | env: {
17 | test: {
18 | // Jest runs in Node and needs CommonJS modules. So does SSR, but Next
19 | // runs webpack on the server as well nowadays
20 | presets: [['next/babel', { 'preset-env': { modules: 'commonjs' } }]]
21 | }
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/tests/jest-interactor/click-callback/test.js:
--------------------------------------------------------------------------------
1 | // highlight{11,16-18}
2 | import React from 'react';
3 | import { Button } from 'shared/components/Button';
4 | import Interactor from '@bigtest/interactor';
5 | import { mount } from '@bigtest/react';
6 |
7 | it('calls "onClick" prop on button click', async () => {
8 | // Render new instance in every test to prevent leaking state
9 | const onClick = jest.fn();
10 | // Mount the component in the DOM
11 | await mount(() => );
12 |
13 | // Use interactor `when` to assert the change has happened
14 | // this will loop until `true` & fails if it isn't true by
15 | // the timeout (2s by default)
16 | await new Interactor('button').click().when(() => {
17 | expect(onClick).toHaveBeenCalled();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/ui/pages/_app.js:
--------------------------------------------------------------------------------
1 | import App, { Container } from 'next/app';
2 | import Head from 'next/head';
3 | import React from 'react';
4 | import { GlobalStyle } from '../global-style';
5 |
6 | export default class MyApp extends App {
7 | static async getInitialProps({ Component, ctx }) {
8 | let pageProps = {};
9 |
10 | if (Component.getInitialProps) {
11 | pageProps = await Component.getInitialProps(ctx);
12 | }
13 |
14 | return { pageProps };
15 | }
16 |
17 | render() {
18 | const { Component, pageProps } = this.props;
19 |
20 | return (
21 |
22 |
23 | React Testing Examples
24 |
25 |
26 |
27 |
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/styled-components/test.js:
--------------------------------------------------------------------------------
1 | // highlight{11-13}
2 | import React from 'react';
3 | import { mount } from 'enzyme';
4 | import { ThemeProvider } from 'styled-components';
5 | import { themeLight } from 'shared/theme';
6 | import { HelloMessageStyled } from 'shared/components/HelloMessageStyled';
7 |
8 | // Hoist helper functions (but not vars) to reuse between test cases
9 | const getWrapper = ({ theme, name }) =>
10 | mount(
11 |
12 |
13 |
14 | );
15 |
16 | it('renders greeting', () => {
17 | // Render new instance in every test to prevent leaking state
18 | const wrapper = getWrapper({ theme: themeLight, name: 'Maggie' });
19 |
20 | expect(wrapper.text()).toMatch(/hello Maggie/i);
21 | });
22 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/react-router/README.md:
--------------------------------------------------------------------------------
1 | ## React Router load and change URL
2 |
3 | The component is connected to [React Router](https://reacttraining.com/react-router/). It renders a variable text containing a URL parameter, as well as a `Link` to another location.
4 |
5 | First we make sure the component renders a param from the initial URL. Then we check that the URL param from a new location is rendered upon clicking on the Link element, which proves that the page has successfully routed.
6 |
7 | > Alternatively, we could just test the `to` prop of the Link element. That's also fine. But this test is closer to how a user thinks: _Click on a link. Did the linked page open?_
8 |
9 | > This type of thinking makes tests more resilient against implementation changes, like upgrading the router library to a new API.
10 |
--------------------------------------------------------------------------------
/tests/jest-rtl/react-router/README.md:
--------------------------------------------------------------------------------
1 | ## React Router load and change URL
2 |
3 | The component is connected to [React Router](https://reacttraining.com/react-router/). It renders a variable text containing a URL parameter, as well as a `Link` to another location.
4 |
5 | First we make sure the component renders a param from the initial URL. Then we check that the URL param from a new location is rendered upon clicking on the Link element, which proves that the page has successfully routed.
6 |
7 | > Alternatively, we could just test the `to` prop of the Link element. That's also fine. But this test is closer to how a user thinks: _Click on a link. Did the linked page open?_
8 |
9 | > This type of thinking makes tests more resilient against implementation changes, like upgrading the router library to a new API.
10 |
--------------------------------------------------------------------------------
/tests/jest-interactor/react-router/README.md:
--------------------------------------------------------------------------------
1 | ## React Router load and change URL
2 |
3 | The component is connected to [React Router](https://reacttraining.com/react-router/). It renders a variable text containing a URL parameter, as well as a `Link` to another location.
4 |
5 | First we make sure the component renders a param from the initial URL. Then we check that the URL param from a new location is rendered upon clicking on the Link element, which proves that the page has successfully routed.
6 |
7 | > Alternatively, we could just test the `to` prop of the Link element. That's also fine. But this test is closer to how a user thinks: _Click on a link. Did the linked page open?_
8 |
9 | > This type of thinking makes tests more resilient against implementation changes, like upgrading the router library to a new API.
10 |
--------------------------------------------------------------------------------
/ui/components/shared/WindowKeyListener.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /* eslint-env browser */
3 |
4 | import { Component } from 'react';
5 |
6 | import type { Node } from 'react';
7 |
8 | type Props = {
9 | children: Node,
10 | onKeyDown: (e: SyntheticKeyboardEvent) => mixed
11 | };
12 |
13 | export class WindowKeyListener extends Component {
14 | componentDidMount() {
15 | window.addEventListener('keydown', this.handleKeyDown);
16 | }
17 |
18 | componentWillUnmount() {
19 | window.removeEventListener('keydown', this.handleKeyDown);
20 | }
21 |
22 | handleKeyDown = (e: SyntheticKeyboardEvent) => {
23 | this.props.onKeyDown(e);
24 | };
25 |
26 | render() {
27 | return this.props.children;
28 | }
29 | }
30 |
31 | export const KEY_S = 83;
32 | export const KEY_ESC = 27;
33 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/xhr/README.md:
--------------------------------------------------------------------------------
1 | ## XHR requests
2 |
3 | The component reads and updates a server counter using the [XHR API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest).
4 |
5 | We test that the component renders the counter value from the mocked API response. Then we click on the increment button, which makes a POST request to increment the counter, and afterwards test that the component renders the incremented value.
6 |
7 | > These tests are **async** because server requests don't resolve immediately. We wait for the button to appear before interacting with our component.
8 |
9 | > We use [`@react-mock/xhr`](https://github.com/skidding/react-mock/tree/master/packages/xhr) to mock the server requests and [`async-retry`](https://github.com/skidding/async-retry) for assertions that take some time to come true.
10 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/fetch/README.md:
--------------------------------------------------------------------------------
1 | ## Fetch requests
2 |
3 | The component reads and updates a server counter using the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
4 |
5 | We test that the component renders the counter value from the mocked API response. Then we click on the increment button, which makes a POST request to increment the counter, and afterwards test that the component renders the incremented value.
6 |
7 | > These tests are **async** because server requests don't resolve immediately. We wait for the button to appear before interacting with our component.
8 |
9 | > We use [`@react-mock/fetch`](https://github.com/skidding/react-mock/tree/master/packages/fetch) to mock the server requests and [`async-retry`](https://github.com/skidding/async-retry) for assertions that take some time to come true.
10 |
--------------------------------------------------------------------------------
/tests/jest-rtl/styled-components/test.js:
--------------------------------------------------------------------------------
1 | // highlight{11-13}
2 | import React from 'react';
3 | import { render, waitForElement } from 'react-testing-library';
4 | import { ThemeProvider } from 'styled-components';
5 | import { themeLight } from 'shared/theme';
6 | import { HelloMessageStyled } from 'shared/components/HelloMessageStyled';
7 |
8 | // Hoist helper functions (but not vars) to reuse between test cases
9 | const renderComponent = ({ theme, name }) =>
10 | render(
11 |
12 |
13 |
14 | );
15 |
16 | it('renders greeting', async () => {
17 | // Render new instance in every test to prevent leaking state
18 | const { getByText } = renderComponent({ theme: themeLight, name: 'Maggie' });
19 |
20 | await waitForElement(() => getByText(/hello Maggie/i));
21 | });
22 |
--------------------------------------------------------------------------------
/ui/components/File/shared.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import rangeParser from 'parse-numeric-range';
4 |
5 | type ParsedCode = {
6 | lineNumsToHighlight: number[],
7 | cleanCode: string
8 | };
9 |
10 | export function parseCode(code: string): ParsedCode {
11 | // The highlight line numbers comments is (optionally) found in the first
12 | // line of a code sample. The comment like this: highlight{2-3,12-13}
13 | const lineNumsToHighlight = getHighlightLines(code);
14 | const cleanCode =
15 | lineNumsToHighlight.length > 0 ? stripFirstLine(code) : code;
16 |
17 | return { lineNumsToHighlight, cleanCode };
18 | }
19 |
20 | function getHighlightLines(code) {
21 | const res = code.match(/highlight\{(.+?)\}/);
22 |
23 | return res ? rangeParser.parse(res[1]) : [];
24 | }
25 |
26 | function stripFirstLine(code) {
27 | return code.replace(/^.+?\n/, '');
28 | }
29 |
--------------------------------------------------------------------------------
/tests/shared/components/ServerXhrCounter.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import axios from 'axios';
3 |
4 | export class ServerCounter extends Component {
5 | state = {
6 | isSyncing: true,
7 | count: 0
8 | };
9 |
10 | componentDidMount() {
11 | axios('/count').then(({ data: { count } }) => {
12 | this.setState({ isSyncing: false, count });
13 | });
14 | }
15 |
16 | increment = () => {
17 | this.setState({ isSyncing: true });
18 |
19 | axios.post('/count').then(({ data: { count } }) => {
20 | this.setState({ isSyncing: false, count });
21 | });
22 | };
23 |
24 | render() {
25 | const { isSyncing, count } = this.state;
26 |
27 | return isSyncing ? (
28 | Syncing...
29 | ) : (
30 |
31 | Clicked {count} times +1
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/ui/global-style.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { createGlobalStyle } from 'styled-components';
4 |
5 | export const GlobalStyle = createGlobalStyle`
6 | html, body {
7 | margin: 0;
8 | padding: 0;
9 | background: #20232a;
10 | color: #888e9c;
11 | font-family: -apple-system, BlinkMacSystemFont, Ubuntu, 'Helvetica Neue', Helvetica, sans-serif;
12 | font-size: 16px;
13 | }
14 |
15 | body.with-modal {
16 | overflow: hidden;
17 | }
18 |
19 | ul, ol, li, p, h1, h2, input, button, select {
20 | margin: 0;
21 | padding: 0;
22 | }
23 |
24 | input, button, select {
25 | font-family: -apple-system, BlinkMacSystemFont, Ubuntu, 'Helvetica Neue', Helvetica, sans-serif;
26 | font-size: 16px;
27 | }
28 |
29 | a {
30 | color: #3058b5;
31 | font-weight: 500;
32 | text-decoration: none;
33 |
34 | :hover {
35 | text-decoration: underline;
36 | }
37 | }
38 | `;
39 |
--------------------------------------------------------------------------------
/ui/components/File/FileHeader.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react';
4 | import styled from 'styled-components';
5 | import { FileActions } from './FileActions';
6 |
7 | type Props = {
8 | name: string,
9 | filePath: string,
10 | code: string
11 | };
12 |
13 | export function FileHeader({ name, filePath, code }: Props) {
14 | return (
15 |
16 | {name}
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | const Container = styled.div`
25 | display: flex;
26 | padding: 0 24px;
27 | height: 40px;
28 | line-height: 40px;
29 | border-radius: 10px;
30 | transition: background 0.3s;
31 | `;
32 |
33 | const Label = styled.div`
34 | flex-grow: 1;
35 | color: rgba(32, 35, 42, 0.7);
36 | font-weight: 500;
37 | `;
38 |
39 | const Actions = styled.div`
40 | flex-grow: 0;
41 | `;
42 |
--------------------------------------------------------------------------------
/ui/components/Footer.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react';
4 | import styled from 'styled-components';
5 | import Credits from '../../CREDITS.md';
6 | import { CenterText, Paragraph } from './shared/styles';
7 |
8 | export function Footer() {
9 | return (
10 |
11 |
12 |
16 | }}
17 | />
18 |
19 | Made with love by{' '}
20 |
21 | Ovidiu
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | const Container = styled.div`
30 | padding: 32px 0 128px 0;
31 | background: #20232a;
32 | color: #888e9c;
33 | overflow: hidden;
34 |
35 | a {
36 | color: #dde0e8;
37 | }
38 | `;
39 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/local-state/test.js:
--------------------------------------------------------------------------------
1 | // highlight{10-12}
2 | import React from 'react';
3 | import { mount } from 'enzyme';
4 | import { StateMock } from '@react-mock/state';
5 | import { StatefulCounter } from 'shared/components/StatefulCounter';
6 |
7 | // Hoist helper functions (but not vars) to reuse between test cases
8 | const getWrapper = ({ count }) =>
9 | mount(
10 |
11 |
12 |
13 | );
14 |
15 | it('renders initial count', () => {
16 | // Render new instance in every test to prevent leaking state
17 | const wrapper = getWrapper({ count: 5 });
18 |
19 | expect(wrapper.text()).toMatch(/clicked 5 times/i);
20 | });
21 |
22 | it('increments count', () => {
23 | // Render new instance in every test to prevent leaking state
24 | const wrapper = getWrapper({ count: 5 });
25 |
26 | wrapper.find('button').simulate('click');
27 | expect(wrapper.text()).toMatch(/clicked 6 times/i);
28 | });
29 |
--------------------------------------------------------------------------------
/tests/shared/components/PersistentForm.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | import React, { Component } from 'react';
4 |
5 | export class PersistentForm extends Component {
6 | state = {
7 | name: 'Guest'
8 | };
9 |
10 | componentDidMount() {
11 | this.setState({
12 | name: localStorage.getItem('name')
13 | });
14 | }
15 |
16 | changeName = e => {
17 | e.preventDefault();
18 |
19 | const name = this.refs.nameField.value;
20 | this.setState({ name }, () => {
21 | localStorage.setItem('name', name);
22 | });
23 | };
24 |
25 | render() {
26 | const { name } = this.state;
27 |
28 | return (
29 | <>
30 | Welcome, {name}!
31 |
36 | >
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/jest-interactor/styled-components/test.js:
--------------------------------------------------------------------------------
1 | // highlight{17,21,23-25}
2 | import React from 'react';
3 | import { mount } from '@bigtest/react';
4 | import { themeLight } from 'shared/theme';
5 | import Interactor from '@bigtest/interactor';
6 | import { ThemeProvider } from 'styled-components';
7 | import { HelloMessageStyled } from 'shared/components/HelloMessageStyled';
8 |
9 | // Hoist helper functions (but not vars) to reuse between test cases
10 | const renderComponent = ({ theme, name }) =>
11 | mount(() => (
12 |
13 |
14 |
15 | ));
16 |
17 | const helloMessage = new Interactor('span');
18 |
19 | it('renders greeting', async () => {
20 | // Render new instance in every test to prevent leaking state
21 | await renderComponent({ theme: themeLight, name: 'Maggie' });
22 |
23 | await helloMessage.when(() =>
24 | expect(helloMessage.text).toEqual('Hello Maggie')
25 | );
26 | });
27 |
--------------------------------------------------------------------------------
/tests/shared/components/ServerFetchCounter.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | import React, { Component } from 'react';
4 |
5 | export class ServerCounter extends Component {
6 | state = {
7 | isSyncing: true,
8 | count: 0
9 | };
10 |
11 | componentDidMount() {
12 | fetch('/count')
13 | .then(res => res.json())
14 | .then(({ count }) => {
15 | this.setState({ isSyncing: false, count });
16 | });
17 | }
18 |
19 | increment = () => {
20 | this.setState({ isSyncing: true });
21 |
22 | fetch('/count', { method: 'POST' })
23 | .then(res => res.json())
24 | .then(({ count }) => {
25 | this.setState({ isSyncing: false, count });
26 | });
27 | };
28 |
29 | render() {
30 | const { isSyncing, count } = this.state;
31 |
32 | return isSyncing ? (
33 | Syncing...
34 | ) : (
35 |
36 | Clicked {count} times +1
37 |
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/ui/components/shared/SectionLink.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react';
4 | import Link from 'next/link';
5 | import queryString from 'query-string';
6 | import { DEFAULT_TEST_KIND_ID } from '../../shared/testKinds';
7 |
8 | import type { Node } from 'react';
9 | import type { TTestKindId } from '../../types';
10 |
11 | type Props = {
12 | children: Node,
13 | testKindId: TTestKindId,
14 | sectionName?: string
15 | };
16 |
17 | export function SectionLink({ children, testKindId, sectionName }: Props) {
18 | const params = { testKindId, sectionName };
19 | const url = getUrl(testKindId, sectionName);
20 |
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | }
27 |
28 | export function getUrl(testKindId: TTestKindId, sectionName?: string) {
29 | if (sectionName) {
30 | return `/${testKindId}/${sectionName}`;
31 | }
32 |
33 | return testKindId === DEFAULT_TEST_KIND_ID ? '/' : `/${testKindId}`;
34 | }
35 |
--------------------------------------------------------------------------------
/ui/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Head, Main, NextScript } from 'next/document';
2 | import { ServerStyleSheet } from 'styled-components';
3 |
4 | export default class MyDocument extends Document {
5 | static getInitialProps({ renderPage }) {
6 | const sheet = new ServerStyleSheet();
7 | const page = renderPage(App => props =>
8 | sheet.collectStyles( )
9 | );
10 | const styleTags = sheet.getStyleElement();
11 |
12 | return { ...page, styleTags };
13 | }
14 |
15 | render() {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 | {this.props.styleTags}
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/ui/next.config.js:
--------------------------------------------------------------------------------
1 | const { addGlobalEntry, addLoaders } = require('./webpack.extend');
2 | const { getTestKindIds, getTestNames } = require('./server/testFiles');
3 |
4 | module.exports = {
5 | webpack: (config, { defaultLoaders }) => {
6 | return addLoaders(addGlobalEntry(config), defaultLoaders.babel);
7 | },
8 | exportPathMap() {
9 | return {
10 | '/': { page: '/' },
11 | '/about': { page: '/about' },
12 | ...getTestPaths()
13 | };
14 | }
15 | };
16 |
17 | function getTestPaths() {
18 | let paths = {};
19 |
20 | getTestKindIds().forEach(testKindId => {
21 | paths[`/${testKindId}`] = { page: '/', query: { testKindId } };
22 | paths[`/${testKindId}/setup`] = {
23 | page: '/',
24 | query: { testKindId, sectionName: 'setup' }
25 | };
26 |
27 | getTestNames(testKindId).forEach(testName => {
28 | paths[`/${testKindId}/${testName}`] = {
29 | page: '/',
30 | query: { testKindId, sectionName: testName }
31 | };
32 | });
33 | });
34 |
35 | return paths;
36 | }
37 |
--------------------------------------------------------------------------------
/tests/jest-rtl/local-state/test.js:
--------------------------------------------------------------------------------
1 | // highlight{10-12}
2 | import React from 'react';
3 | import { render, waitForElement, fireEvent } from 'react-testing-library';
4 | import { StateMock } from '@react-mock/state';
5 | import { StatefulCounter } from 'shared/components/StatefulCounter';
6 |
7 | // Hoist helper functions (but not vars) to reuse between test cases
8 | const renderComponent = ({ count }) =>
9 | render(
10 |
11 |
12 |
13 | );
14 |
15 | it('renders initial count', async () => {
16 | // Render new instance in every test to prevent leaking state
17 | const { getByText } = renderComponent({ count: 5 });
18 |
19 | await waitForElement(() => getByText(/clicked 5 times/i));
20 | });
21 |
22 | it('increments count', async () => {
23 | // Render new instance in every test to prevent leaking state
24 | const { getByText } = renderComponent({ count: 5 });
25 |
26 | fireEvent.click(getByText('+1'));
27 | await waitForElement(() => getByText(/clicked 6 times/i));
28 | });
29 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/redux/test.js:
--------------------------------------------------------------------------------
1 | // highlight{12-14}
2 | import React from 'react';
3 | import { createStore } from 'redux';
4 | import { Provider } from 'react-redux';
5 | import { mount } from 'enzyme';
6 | import { ReduxCounter } from 'shared/components/ReduxCounter';
7 | import { counterReducer } from './reducer';
8 |
9 | // Hoist helper functions (but not vars) to reuse between test cases
10 | const getWrapper = ({ count }) =>
11 | mount(
12 |
13 |
14 |
15 | );
16 |
17 | it('renders initial count', () => {
18 | // Render new instance in every test to prevent leaking state
19 | const wrapper = getWrapper({ count: 5 });
20 |
21 | expect(wrapper.text()).toMatch(/clicked 5 times/i);
22 | });
23 |
24 | it('increments count', () => {
25 | // Render new instance in every test to prevent leaking state
26 | const wrapper = getWrapper({ count: 5 });
27 |
28 | wrapper.find('button').simulate('click');
29 | expect(wrapper.text()).toMatch(/clicked 6 times/i);
30 | });
31 |
--------------------------------------------------------------------------------
/ui/cosmos-proxies/next.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import App, { Container } from 'next/app';
3 | import { createRouter } from 'next/router';
4 |
5 | // XXX I don't know what I'm doing here! Here are some files I sniffed thru:
6 | // - https://github.com/zeit/next.js/blob/965f50beb238eab8aafa9be32833b0e7d2947574/packages/next/client/index.js#L94-L101
7 | // - https://github.com/zeit/next.js/blob/82d56e063aad12ac8fee5b9d5ed24ccf725b1a5b/packages/next-server/lib/router/index.js#L98-L103
8 | // - https://github.com/zeit/next.js/blob/785377d3c306b5c89c566f59a846b7829e627654/packages/next/pages/_app.js#L17-L23
9 | createRouter('/', {}, '/');
10 |
11 | class NextApp extends App {
12 | render() {
13 | return {this.props.children} ;
14 | }
15 | }
16 |
17 | export function NextRouterProxy({ nextProxy, ...otherProps }) {
18 | const { value: NextProxy, next } = nextProxy;
19 |
20 | return (
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/ui/components/File/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { Component } from 'react';
4 | import styled from 'styled-components';
5 | import { FileOptions } from '../../contexts';
6 | import { Center } from '../shared/styles';
7 | import { Code } from './Code';
8 | import { FileHeader } from './FileHeader';
9 |
10 | type Props = {
11 | name: string,
12 | filePath: string,
13 | code: string
14 | };
15 |
16 | export class File extends Component {
17 | render() {
18 | const { name, filePath, code } = this.props;
19 |
20 | return (
21 |
22 |
23 | {({ showComments, showImports }) => (
24 | <>
25 |
26 |
31 | >
32 | )}
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | const Container = styled(Center)`
40 | margin-bottom: 16px;
41 | `;
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Ovidiu Cherecheș
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/jest-rtl/redux/test.js:
--------------------------------------------------------------------------------
1 | // highlight{12-14}
2 | import React from 'react';
3 | import { createStore } from 'redux';
4 | import { Provider } from 'react-redux';
5 | import { render, waitForElement, fireEvent } from 'react-testing-library';
6 | import { ReduxCounter } from 'shared/components/ReduxCounter';
7 | import { counterReducer } from './reducer';
8 |
9 | // Hoist helper functions (but not vars) to reuse between test cases
10 | const renderComponent = ({ count }) =>
11 | render(
12 |
13 |
14 |
15 | );
16 |
17 | it('renders initial count', async () => {
18 | // Render new instance in every test to prevent leaking state
19 | const { getByText } = renderComponent({ count: 5 });
20 |
21 | await waitForElement(() => getByText(/clicked 5 times/i));
22 | });
23 |
24 | it('increments count', async () => {
25 | // Render new instance in every test to prevent leaking state
26 | const { getByText } = renderComponent({ count: 5 });
27 |
28 | fireEvent.click(getByText('+1'));
29 | await waitForElement(() => getByText(/clicked 6 times/i));
30 | });
31 |
--------------------------------------------------------------------------------
/tests/jest-rtl/react-router/test.js:
--------------------------------------------------------------------------------
1 | // highlight{10-14,31-33}
2 | import React from 'react';
3 | import { MemoryRouter, Route } from 'react-router';
4 | import { render, waitForElement, fireEvent } from 'react-testing-library';
5 | import { UserWithRouter } from 'shared/components/UserWithRouter';
6 |
7 | // Hoist helper functions (but not vars) to reuse between test cases
8 | const renderComponent = ({ userId }) =>
9 | render(
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | it('renders initial user id', async () => {
18 | // Render new instance in every test to prevent leaking state
19 | const { getByText } = renderComponent({ userId: 5 });
20 |
21 | await waitForElement(() => getByText(/user #5/i));
22 | });
23 |
24 | it('renders next user id', async () => {
25 | // Render new instance in every test to prevent leaking state
26 | const { getByText } = renderComponent({ userId: 5 });
27 |
28 | fireEvent.click(getByText(/next user/i));
29 | await waitForElement(() => getByText(/user #6/i));
30 | });
31 |
--------------------------------------------------------------------------------
/tests/jest-interactor/README.md:
--------------------------------------------------------------------------------
1 | ## Setup
2 |
3 | Minimal setup required to use [@bigtest/interactor](https://github.com/bigtestjs/interactor) with [Jest](https://jestjs.io/).
4 |
5 | [Interactors](https://www.bigtestjs.io/docs/interactor/) define a part
6 | of an app that tests act upon. They are immutable, reusable, and
7 | composable. Which means you can write expressive tests that are fast,
8 | robust, and match the composibility of the components you're testing.
9 |
10 | Typically, when writing tests for your components, you will use a
11 | custom interactor (like `counter-interactor.js` below). This allows
12 | you to define all the ways someone _interacts_ with your
13 | component. Can they type in it? Click it? Drag it? This is all done in
14 | a way that doesn't tie you to a framework or implementation
15 | details. Interactors are able to do that because they're _converging_
16 | on the state of the DOM. They're not aware of your components
17 | implentation, only what is reflected in the DOM.
18 |
19 | If you don't need a custom interactor, you can import the `Interactor`
20 | class and pass the selector of the element you want to interact with.
21 |
22 | > All examples featured here run using these exact config files.
23 |
--------------------------------------------------------------------------------
/ui/components/Section/TitleLink.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react';
4 | import styled from 'styled-components';
5 | import svgLink from '../../svg/link.svg';
6 | import { SectionLink } from '../shared/SectionLink';
7 | import { H2 } from '../shared/styles';
8 |
9 | import type { Node } from 'react';
10 | import type { TTestKindId } from '../../types';
11 |
12 | type Props = {
13 | children: Node,
14 | testKindId: TTestKindId,
15 | sectionName: string
16 | };
17 |
18 | export function TitleLink({ children, testKindId, sectionName }: Props) {
19 | return (
20 |
21 |
22 |
23 |
24 | {children}
25 |
26 | );
27 | }
28 |
29 | const Container = styled(H2)`
30 | a {
31 | position: absolute;
32 | width: 24px;
33 | height: 24px;
34 | padding: 4px;
35 | margin-left: -32px;
36 |
37 | :focus {
38 | outline: none;
39 | }
40 | }
41 |
42 | :hover a {
43 | background: url(${svgLink});
44 | background-size: 24px;
45 | background-position: calc(100% - 4px) 4px;
46 | background-repeat: no-repeat;
47 | }
48 | `;
49 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": [
4 | "eslint:recommended",
5 | "plugin:import/errors",
6 | "plugin:import/warnings",
7 | "plugin:flowtype/recommended"
8 | ],
9 | "plugins": ["react", "flowtype"],
10 | "env": {
11 | "es6": true,
12 | "shared-node-browser": true
13 | },
14 | "settings": {
15 | "import/resolver": {
16 | "babel-module": {}
17 | }
18 | },
19 | "rules": {
20 | "react/jsx-uses-react": "error",
21 | "react/jsx-uses-vars": "error"
22 | },
23 | "overrides": [
24 | {
25 | "files": [
26 | "babel.config.js",
27 | "jest.config.js",
28 | "cosmos.config.js",
29 | "next.config.js",
30 | "webpack.extend.js",
31 | "ui/webpack-loaders/**/*.js",
32 | "ui/server/**/*.js"
33 | ],
34 | "env": {
35 | "node": true
36 | },
37 | "rules": {
38 | "no-console": "off"
39 | }
40 | },
41 | {
42 | "files": ["**/test.js", "**/*.test.js"],
43 | "env": {
44 | "jest": true
45 | }
46 | },
47 | {
48 | "files": ["**/localstorage/test.js"],
49 | "env": {
50 | "browser": true
51 | }
52 | }
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/ui/webpack-loaders/readme-text-loader.test.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import fs from 'fs';
4 | import loader from './readme-text-loader';
5 |
6 | const readme = fs.readFileSync(
7 | './tests/jest-enzyme/react-router/README.md',
8 | 'utf8'
9 | );
10 | const output = loader(readme);
11 | const json = JSON.parse(output.replace(/module.exports = {(.+)}/, '{$1}'));
12 |
13 | it('extracts title', () => {
14 | expect(json.title).toBe('React Router load and change URL');
15 | });
16 |
17 | it('extracts body', () => {
18 | expect(json.body).toEqual([
19 | 'The component is connected to React Router. It renders a variable text containing a URL parameter, as well as a Link to another location.',
20 | 'First we make sure the component renders a param from the initial URL. Then we check that the URL param from a new location is rendered upon clicking on the Link element, which proves that the page has successfully routed.',
21 | `Alternatively, we could just test the to prop of the Link element. That's also fine. But this test is closer to how a user thinks: Click on a link. Did the linked page open?`,
22 | `This type of thinking makes tests more resilient against implementation changes, like upgrading the router library to a new API.`
23 | ]);
24 | });
25 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/react-router/test.js:
--------------------------------------------------------------------------------
1 | // highlight{10-14,31-33}
2 | import React from 'react';
3 | import { MemoryRouter, Route } from 'react-router';
4 | import { mount } from 'enzyme';
5 | import { UserWithRouter } from 'shared/components/UserWithRouter';
6 |
7 | // Hoist helper functions (but not vars) to reuse between test cases
8 | const getWrapper = ({ userId }) =>
9 | mount(
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | it('renders initial user id', () => {
18 | // Render new instance in every test to prevent leaking state
19 | const wrapper = getWrapper({ userId: 5 });
20 |
21 | expect(wrapper.text()).toMatch(/user #5/i);
22 | });
23 |
24 | it('renders next user id', () => {
25 | // Render new instance in every test to prevent leaking state
26 | const wrapper = getWrapper({ userId: 5 });
27 |
28 | wrapper
29 | .find('a')
30 | .find({ children: 'Next user' })
31 | // RR Link ignores clicks if event.button isn't 0 (eg. right click events)
32 | // https://github.com/airbnb/enzyme/issues/516
33 | .simulate('click', { button: 0 });
34 |
35 | expect(wrapper.text()).toMatch(/user #6/i);
36 | });
37 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing guide
2 |
3 | First make sure you understand [what these examples focus on](https://github.com/skidding/react-testing-examples/blob/master/WHATSTHIS.md#whats-the-focus). Then make the examples as **concise** as possible. Finally, try to communicate your intentions.
4 |
5 | ### How to add a test
6 |
7 | Here are the required steps for adding a _foobar_ example in the `jest-enzyme` collection.
8 |
9 | - Put test(s) in `tests/jest-enzyme/foobar/test.js`
10 | - Put description in `tests/jest-enzyme/foobar/README.md`
11 |
12 | Get inspired from existing examples.
13 |
14 | ### How to add a test collection (ie. a new toolchain)
15 |
16 | > This one's a big deal so you want to start a conversation before jumping into it.
17 |
18 | Copy one of the dirs from [tests](tests) (eg. `jest-rtl`) to get started. You don't have to mirror tests from existing collections. At least one test example is enough to create a new collection.
19 |
20 | A few more steps required before deploying a new collection:
21 |
22 | - Add a label [here](https://github.com/skidding/react-testing-examples/blob/bd01b2e8a2a5f9fd7446e781f2026e191326afcf/ui/shared/testKinds.js#L8-L9)
23 | - Add test commands [here](https://github.com/skidding/react-testing-examples/blob/master/package.json#L4-L7)
24 |
--------------------------------------------------------------------------------
/ui/components/SectionList/ToggleButton.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react';
4 | import styled from 'styled-components';
5 |
6 | import type { Node } from 'react';
7 |
8 | type Props = {
9 | label: Node,
10 | actions?: Node,
11 | isOpen: boolean,
12 | onClick: () => mixed
13 | };
14 |
15 | export function ToggleButton({ label, actions, isOpen, onClick }: Props) {
16 | return (
17 |
18 |
19 | {label} {isOpen ? '↑' : '↓'}
20 |
21 | {actions && {actions}
}
22 |
23 | );
24 | }
25 |
26 | const Container = styled.div`
27 | display: flex;
28 | padding: 0 0;
29 | height: 40px;
30 | line-height: 40px;
31 | border-radius: 10px;
32 | user-select: none;
33 | transition: background 0.3s;
34 | text-transform: uppercase;
35 |
36 | .label {
37 | font-weight: 700;
38 | }
39 |
40 | .button {
41 | flex-grow: 1;
42 | cursor: pointer;
43 | opacity: ${props => (props.isOpen ? 0.6 : 0.8)};
44 | transition: opacity 0.3s;
45 |
46 | :hover .label {
47 | text-decoration: underline;
48 | }
49 | }
50 |
51 | .actions {
52 | flex-grow: 0;
53 | }
54 | `;
55 |
--------------------------------------------------------------------------------
/tests/jest-interactor/local-state/test.js:
--------------------------------------------------------------------------------
1 | // highlight{17,21,23-25,30,34-36}
2 | import React from 'react';
3 | import { mount } from '@bigtest/react';
4 | import Counter from '../counter-interactor';
5 | import { StateMock } from '@react-mock/state';
6 | import { StatefulCounter } from 'shared/components/StatefulCounter';
7 |
8 | // Hoist helper functions (but not vars) to reuse between test cases
9 | const renderComponent = ({ count }) =>
10 | mount(() => (
11 |
12 |
13 |
14 | ));
15 |
16 | // reuse the custom interactor for the same type of component
17 | let counter = new Counter();
18 |
19 | it('renders initial count', async () => {
20 | // Render new instance in every test to prevent leaking state
21 | await renderComponent({ count: 5 });
22 |
23 | await counter.when(() =>
24 | expect(counter.clickedText).toContain('Clicked 5 times')
25 | );
26 | });
27 |
28 | it('increments count', async () => {
29 | // Render new instance in every test to prevent leaking state
30 | await renderComponent({ count: 5 });
31 |
32 | // Interactions from interactor are chainable,
33 | // we can increment and then assert in the same chain.
34 | await counter
35 | .increment()
36 | .when(() => expect(counter.clickedText).toContain('Clicked 6 times'));
37 | });
38 |
--------------------------------------------------------------------------------
/ui/components/SectionList/fixtures.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { createFixture } from 'react-cosmos-classic';
4 | import { SectionList } from '.';
5 | import { testKinds } from '../../import-files';
6 |
7 | export default [
8 | createFixture({
9 | name: 'open',
10 | component: SectionList,
11 | props: {
12 | sections: testKinds['jest-enzyme'].tests,
13 | testKindId: 'jest-enzyme',
14 | sectionName: undefined,
15 | searchText: '',
16 | changeSearch: () => {}
17 | },
18 | state: {
19 | isOpen: true
20 | },
21 | bgColor: '#f5f7f9'
22 | }),
23 | createFixture({
24 | name: 'closed',
25 | component: SectionList,
26 | props: {
27 | sections: testKinds['jest-enzyme'].tests,
28 | testKindId: 'jest-enzyme',
29 | sectionName: undefined,
30 | searchText: '',
31 | changeSearch: () => {}
32 | },
33 | state: {
34 | isOpen: false
35 | },
36 | bgColor: '#f5f7f9'
37 | }),
38 | createFixture({
39 | name: 'search',
40 | component: SectionList,
41 | props: {
42 | sections: testKinds['jest-enzyme'].tests,
43 | testKindId: 'jest-enzyme',
44 | sectionName: undefined,
45 | searchText: 'state',
46 | changeSearch: () => {}
47 | },
48 | state: {
49 | isOpen: false
50 | },
51 | bgColor: '#f5f7f9'
52 | })
53 | ];
54 |
--------------------------------------------------------------------------------
/ui/webpack-loaders/readme-text-loader.js:
--------------------------------------------------------------------------------
1 | const unified = require('unified');
2 | const remark = require('remark-parse');
3 | const traverse = require('traverse');
4 |
5 | module.exports = function parseReadme(source) {
6 | const tree = unified()
7 | .use(remark)
8 | .parse(source);
9 |
10 | const info = {
11 | title: getTitle(tree.children),
12 | body: getBody(tree.children)
13 | };
14 |
15 | return `module.exports = ${JSON.stringify(info)}`;
16 | };
17 |
18 | function getTitle(nodes) {
19 | const node = nodes.find(n => n.type === 'heading' && n.depth === 2);
20 | if (!node || !node.children[0]) {
21 | return '';
22 | }
23 |
24 | return node.children[0].value;
25 | }
26 |
27 | function getBody(nodes) {
28 | return traverse.reduce(
29 | nodes,
30 | (ps, node) => {
31 | return isParagraphNode(node) ? [...ps, getParagraphText(node)] : ps;
32 | },
33 | []
34 | );
35 | }
36 |
37 | function getParagraphText(node) {
38 | return traverse.reduce(
39 | node.children,
40 | (text, node) => (isTextNode(node) ? text + node.value : text),
41 | ''
42 | );
43 | }
44 |
45 | const TEXT_NODES = ['text', 'inlineCode'];
46 |
47 | function isParagraphNode(node) {
48 | return node && node.type === 'paragraph';
49 | }
50 |
51 | function isTextNode(node) {
52 | return node && TEXT_NODES.indexOf(node.type) !== -1;
53 | }
54 |
--------------------------------------------------------------------------------
/ui/search.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { match, score } from 'fuzzaldrin-plus';
4 |
5 | import type { TReadmeMeta, TSection } from './types';
6 |
7 | export function shouldSearch(searchText: string) {
8 | return searchText.length > 2;
9 | }
10 |
11 | export function matchReadmeText(
12 | { title, body }: TReadmeMeta,
13 | searchText: string
14 | ) {
15 | const searchTextNorm = normalizeTxt(searchText);
16 |
17 | return (
18 | match(title, searchText).length > 0 ||
19 | body.some(p => normalizeTxt(p).indexOf(searchTextNorm) !== -1)
20 | );
21 | }
22 |
23 | export function sortSections(sections: TSection[], searchText: string) {
24 | const sorted: Array = [...sections]
25 | .sort((a, b) => scoreByBody(b, searchText) - scoreByBody(a, searchText))
26 | .sort((a, b) => scoreByTitle(b, searchText) - scoreByTitle(a, searchText));
27 |
28 | return sorted;
29 | }
30 |
31 | function normalizeTxt(txt: string): string {
32 | return txt.toLowerCase().replace(/\s/g, '');
33 | }
34 |
35 | function scoreByTitle(section: TSection, searchText: string): number {
36 | const { title } = section.readme.meta;
37 |
38 | return score(title, searchText);
39 | }
40 |
41 | function scoreByBody(section: TSection, searchText: string): number {
42 | const { body } = section.readme.meta;
43 |
44 | return Math.max(0, ...body.map(p => score(p, searchText)));
45 | }
46 |
--------------------------------------------------------------------------------
/tests/jest-interactor/redux/test.js:
--------------------------------------------------------------------------------
1 | // highlight{19,23,25-27,32,36-38}
2 | import React from 'react';
3 | import { createStore } from 'redux';
4 | import { Provider } from 'react-redux';
5 | import { mount } from '@bigtest/react';
6 | import Counter from '../counter-interactor';
7 | import { ReduxCounter } from 'shared/components/ReduxCounter';
8 | import { counterReducer } from './reducer';
9 |
10 | // Hoist helper functions (but not vars) to reuse between test cases
11 | const renderComponent = ({ count }) =>
12 | mount(() => (
13 |
14 |
15 |
16 | ));
17 |
18 | // reuse the custom interactor for the same type of component
19 | let counter = new Counter();
20 |
21 | it('renders initial count', async () => {
22 | // Render new instance in every test to prevent leaking state
23 | await renderComponent({ count: 5 });
24 |
25 | await counter.when(() =>
26 | expect(counter.clickedText).toContain('Clicked 5 times')
27 | );
28 | });
29 |
30 | it('increments count', async () => {
31 | // Render new instance in every test to prevent leaking state
32 | await renderComponent({ count: 5 });
33 |
34 | // Interactions from interactor are chainable,
35 | // we can increment and then assert in the same chain.
36 | await counter
37 | .increment()
38 | .when(() => expect(counter.text).toContain('Clicked 6 times'));
39 | });
40 |
--------------------------------------------------------------------------------
/ui/components/Section/Readme.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { Component } from 'react';
4 | import styled from 'styled-components';
5 | import { Paragraph, Blockquote, InlineCode } from '../shared/styles';
6 | import { FuzzyHighlighter } from '../shared/FuzzyHighlighter';
7 | import { TitleLink } from './TitleLink';
8 |
9 | import type { TTestKindId, TReadme } from '../../types';
10 |
11 | type Props = {
12 | testKindId: TTestKindId,
13 | sectionName: string,
14 | readme: TReadme,
15 | searchText: string
16 | };
17 |
18 | export class Readme extends Component {
19 | render() {
20 | const { testKindId, sectionName, readme, searchText } = this.props;
21 |
22 | return (
23 |
24 | (
27 |
28 |
32 |
33 | ),
34 | p: Paragraph,
35 | blockquote: Blockquote,
36 | inlineCode: InlineCode,
37 | a: props =>
38 | }}
39 | />
40 |
41 | );
42 | }
43 | }
44 |
45 | export const Container = styled.div`
46 | margin-bottom: 24px;
47 | `;
48 |
--------------------------------------------------------------------------------
/tests/jest-interactor/react-router/test.js:
--------------------------------------------------------------------------------
1 | // highlight{19-23,25,29,31,36,39}
2 | import React from 'react';
3 | import { MemoryRouter, Route } from 'react-router';
4 | import { mount } from '@bigtest/react';
5 | import { interactor, clickable } from '@bigtest/interactor';
6 | import { UserWithRouter } from 'shared/components/UserWithRouter';
7 |
8 | // Hoist helper functions (but not vars) to reuse between test cases
9 | const renderComponent = ({ userId }) =>
10 | mount(() => (
11 |
12 |
13 |
14 |
15 |
16 | ));
17 |
18 | // Create custom interactor for interacting with this page
19 | @interactor
20 | class UserPage {
21 | // click the next user link
22 | next = clickable('a');
23 | }
24 |
25 | let page = new UserPage();
26 |
27 | it('renders initial user id', async () => {
28 | // Render new instance in every test to prevent leaking state
29 | await renderComponent({ userId: 5 });
30 |
31 | await page.when(() => expect(page.text).toContain('User #5'));
32 | });
33 |
34 | it('renders next user id', async () => {
35 | // Render new instance in every test to prevent leaking state
36 | await renderComponent({ userId: 5 });
37 |
38 | // click the next link and assert the page route changed
39 | await page.next().when(() => expect(page.text).toContain('User #6'));
40 | });
41 |
--------------------------------------------------------------------------------
/ui/webpack.extend.js:
--------------------------------------------------------------------------------
1 | // XXX: Kids, don't try this at home, but it seems this is an entry that is
2 | // loaded on both server and client
3 | const UNIVERSAL_APP_ENTRY_MATCH = 'static/.+?/pages/_app.js';
4 |
5 | module.exports = {
6 | addGlobalEntry(config) {
7 | const origEntry = config.entry;
8 | const entry = async () => {
9 | const entries = await origEntry();
10 |
11 | const entryNames = Object.keys(entries);
12 | const appEntry = entryNames.find(e => e.match(UNIVERSAL_APP_ENTRY_MATCH));
13 |
14 | if (!appEntry) {
15 | return entries;
16 | }
17 |
18 | return {
19 | ...entries,
20 | [appEntry]: [require.resolve('./global'), ...entries[appEntry]]
21 | };
22 | };
23 |
24 | return {
25 | ...config,
26 | entry
27 | };
28 | },
29 |
30 | addLoaders: function(config, babelLoader) {
31 | const { module } = config;
32 | const { rules } = module;
33 |
34 | const mdxRule = {
35 | test: /(README|SETUP|WHATSTHIS|CREDITS).md$/,
36 | use: [babelLoader, '@mdx-js/loader']
37 | };
38 | const importFilesRule = {
39 | test: require.resolve('./import-files'),
40 | use: require.resolve('./webpack-loaders/import-tests-loader')
41 | };
42 |
43 | return {
44 | ...config,
45 | module: {
46 | ...module,
47 | rules: [...rules, mdxRule, importFilesRule]
48 | }
49 | };
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/ui/components/App.fixture.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { createFixture } from 'react-cosmos-classic';
4 | import { App } from './App';
5 | import { gitRef } from '../import-files';
6 | import { getTestKind } from '../shared/testKinds';
7 |
8 | export default [
9 | createFixture({
10 | name: 'react-testing-library',
11 | component: App,
12 | props: {
13 | gitRef,
14 | testKind: getTestKind('jest-rtl'),
15 | showAbout: false
16 | },
17 | state: {}
18 | }),
19 | createFixture({
20 | name: 'react-testing-library Fetch',
21 | component: App,
22 | props: {
23 | gitRef,
24 | testKind: getTestKind('jest-rtl'),
25 | sectionName: 'fetch',
26 | showAbout: false
27 | },
28 | state: {}
29 | }),
30 | createFixture({
31 | name: 'Enzyme',
32 | component: App,
33 | props: {
34 | gitRef,
35 | testKind: getTestKind('jest-enzyme'),
36 | showAbout: false
37 | },
38 | state: {}
39 | }),
40 | createFixture({
41 | name: 'Enzyme setup',
42 | component: App,
43 | props: {
44 | gitRef,
45 | testKind: getTestKind('jest-enzyme'),
46 | sectionName: 'setup',
47 | showAbout: false
48 | },
49 | state: {}
50 | }),
51 | createFixture({
52 | name: 'About',
53 | component: App,
54 | props: {
55 | gitRef,
56 | testKind: getTestKind('jest-rtl'),
57 | showAbout: true
58 | }
59 | })
60 | ];
61 |
--------------------------------------------------------------------------------
/ui/components/Section/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react';
4 | import styled from 'styled-components';
5 | import { CenterText } from '../shared/styles';
6 | import { File } from '../File';
7 | import { Readme } from './Readme';
8 |
9 | import type { TTestKindId, TSection } from '../../types';
10 |
11 | type Props = {
12 | testKindId: TTestKindId,
13 | section: TSection,
14 | searchText: string
15 | };
16 |
17 | export function Section({ testKindId, section, searchText }: Props) {
18 | const { name, readme, files } = section;
19 |
20 | return (
21 |
22 |
23 |
29 |
30 | {Object.keys(files).map(filePath => (
31 |
37 | ))}
38 |
39 | );
40 | }
41 |
42 | function getFilePath(testKindId, sectionName, filePath) {
43 | return sectionName === 'setup'
44 | ? `${testKindId}/${filePath}`
45 | : `${testKindId}/${sectionName}/${filePath}`;
46 | }
47 |
48 | const SectionContainer = styled.div`
49 | margin-top: 48px;
50 | background: #f5f7f9;
51 | color: #20232a;
52 | `;
53 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Javascript Node CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details
4 | #
5 | version: 2
6 | jobs:
7 | build:
8 | docker:
9 | # specify the version you desire here
10 | - image: circleci/node:8
11 | # Specify service dependencies here if necessary
12 | # CircleCI maintains a library of pre-built images
13 | # documented at https://circleci.com/docs/2.0/circleci-images/
14 | # - image: circleci/mongo:3.4.4
15 |
16 | working_directory: ~/repo
17 |
18 | steps:
19 | - checkout
20 |
21 | # Download and cache dependencies
22 | - restore_cache:
23 | keys:
24 | - v1-dependencies-{{ checksum "package.json" }}
25 | # fallback to using the latest cache if no exact match is found
26 | - v1-dependencies-
27 |
28 | - run: yarn install
29 |
30 | - save_cache:
31 | paths:
32 | - node_modules
33 | key: v1-dependencies-{{ checksum "package.json" }}
34 |
35 | # Check code
36 | - run: yarn lint
37 | - run: yarn flow
38 |
39 | # Test code
40 | - run: yarn test:ci
41 | # The resource_class feature allows configuring CPU and RAM resources for each job. Different resource classes are available for different executors. https://circleci.com/docs/2.0/configuration-reference/#resourceclass
42 | resource_class: large
43 |
--------------------------------------------------------------------------------
/WHATSTHIS.md:
--------------------------------------------------------------------------------
1 | ## Dear friend,
2 |
3 | Presenting these examples took work. I hope they'll make your life easier!
4 |
5 | ### Why put this together?
6 |
7 | There's a lot of wisdom that goes into writing clean tests. With every project I learn something new. I wanted to document my latest testing style and use it as a go-to resource for future projects.
8 |
9 | ### How does it work?
10 |
11 | The test examples are up to date and run in [CircleCI](https://circleci.com/gh/skidding/react-testing-examples). This searchable library is generated from README and test files [available on GitHub](https://github.com/skidding/react-testing-examples/tree/master/tests).
12 |
13 | ### Is this useful to you?
14 |
15 | The testing examples are _opinionated_. They aim to mimic user behavior and avoid testing abstract components. But you're free to disagree with [my testing philosophy](https://medium.com/@skidding/testing-react-components-30516bc6a1b3). The examples also feature modern libraries and agnostic testing techniques.
16 |
17 | ### What's the focus?
18 |
19 | _The component setup_. Performing actions and assertions is already well documented by the tools that handle event simulation and/or expectations. The examples here focus on how to wire up various component types for testing.
20 |
21 | ### Want to contribute?
22 |
23 | Let's harness our collective knowledge to create a great resource for testing React components!
24 |
25 | Best,
26 | [Ovidiu](https://twitter.com/skidding)
27 |
--------------------------------------------------------------------------------
/ui/components/shared/FuzzyHighlighter.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react';
4 | import styled from 'styled-components';
5 | import { match } from 'fuzzaldrin-plus';
6 | import { shouldSearch } from '../../search';
7 |
8 | type Props = {
9 | searchText: string,
10 | targetText: string
11 | };
12 |
13 | export function FuzzyHighlighter({ searchText, targetText }: Props) {
14 | if (!shouldSearch(searchText)) {
15 | return targetText;
16 | }
17 |
18 | const fuzzyMatch = match(targetText, searchText);
19 | if (fuzzyMatch.length === 0) {
20 | return targetText;
21 | }
22 |
23 | const chars = [];
24 | fuzzyMatch.forEach((hlIndex, index) => {
25 | // If the first character isn't highlighted, push the initial
26 | // unhighlighted characters
27 | if (index === 0 && hlIndex !== 0) {
28 | chars.push(targetText.slice(0, hlIndex));
29 | }
30 |
31 | // Push the highlighted character
32 | const hlChar = targetText.slice(hlIndex, hlIndex + 1);
33 | chars.push({hlChar} );
34 |
35 | // If the next character isn't highlighted,
36 | // push the subsequent unhighlighted characters
37 | const nextHlIndex = fuzzyMatch[index + 1];
38 | if (nextHlIndex !== hlIndex + 1) {
39 | chars.push(targetText.slice(hlIndex + 1, nextHlIndex));
40 | }
41 | });
42 |
43 | return chars;
44 | }
45 |
46 | const Mark = styled.mark`
47 | background-color: rgba(255, 229, 100, 0.5);
48 | color: inherit;
49 | `;
50 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/localstorage/test.js:
--------------------------------------------------------------------------------
1 | // highlight{10-12}
2 | import React from 'react';
3 | import { mount } from 'enzyme';
4 | import { LocalStorageMock } from '@react-mock/localstorage';
5 | import { PersistentForm } from 'shared/components/PersistentForm';
6 |
7 | // Hoist helper functions (but not vars) to reuse between test cases
8 | const getWrapper = ({ name }) =>
9 | mount(
10 |
11 |
12 |
13 | );
14 |
15 | const submitForm = ({ wrapper, name }) => {
16 | wrapper.find('input').instance().value = name;
17 | wrapper.find('button').simulate('submit');
18 | };
19 |
20 | it('renders cached name', () => {
21 | // Render new instance in every test to prevent leaking state
22 | const wrapper = getWrapper({ name: 'Trent' });
23 |
24 | expect(wrapper.text()).toMatch(/welcome, Trent/i);
25 | });
26 |
27 | describe('on update', () => {
28 | it('renders updated name', () => {
29 | // Render new instance in every test to prevent leaking state
30 | const wrapper = getWrapper({ name: 'Trent' });
31 | submitForm({ wrapper, name: 'Trevor' });
32 |
33 | expect(wrapper.text()).toMatch(/welcome, Trevor/i);
34 | });
35 |
36 | it('updates LocalStorage cache', () => {
37 | // Render new instance in every test to prevent leaking state
38 | const wrapper = getWrapper({ name: 'Trent' });
39 | submitForm({ wrapper, name: 'Trevor' });
40 |
41 | expect(localStorage.getItem('name')).toBe('Trevor');
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/ui/components/Header/Checkbox.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react';
4 | import styled from 'styled-components';
5 | import { FileOptions } from '../../contexts';
6 |
7 | type FileOptionCheckboxProps = {
8 | onToggle: () => mixed
9 | };
10 |
11 | export function CommentsCheckbox({ onToggle }: FileOptionCheckboxProps) {
12 | return (
13 |
14 | {({ showComments }) => (
15 |
16 | )}
17 |
18 | );
19 | }
20 |
21 | export function ImportsCheckbox({ onToggle }: FileOptionCheckboxProps) {
22 | return (
23 |
24 | {({ showImports }) => (
25 |
26 | )}
27 |
28 | );
29 | }
30 |
31 | type CheckboxProps = {
32 | name: string,
33 | checked: boolean,
34 | onToggle: () => mixed
35 | };
36 |
37 | function Checkbox({ name, checked, onToggle }: CheckboxProps) {
38 | return (
39 |
40 |
41 | show {name}
42 |
43 | );
44 | }
45 |
46 | const CheckboxLabel = styled.label`
47 | display: flex;
48 | align-items: center;
49 | margin-right: 12px;
50 | line-height: 24px;
51 | user-select: none;
52 |
53 | :last-child {
54 | margin-right: 0;
55 | }
56 |
57 | input {
58 | margin-left: 2px;
59 | margin-right: 6px;
60 | }
61 | `;
62 |
--------------------------------------------------------------------------------
/tests/jest-rtl/fetch/test.js:
--------------------------------------------------------------------------------
1 | // highlight{10-17,33-36}
2 | import React from 'react';
3 | import { render, fireEvent, waitForElement } from 'react-testing-library';
4 | import { FetchMock } from '@react-mock/fetch';
5 | import { ServerCounter } from 'shared/components/ServerFetchCounter';
6 |
7 | // Hoist helper functions (but not vars) to reuse between test cases
8 | const renderComponent = ({ count }) =>
9 | render(
10 |
16 |
17 |
18 | );
19 |
20 | it('renders initial count', async () => {
21 | // Render new instance in every test to prevent leaking state
22 | const { getByText } = renderComponent({ count: 5 });
23 |
24 | // It takes time for the counter to appear because
25 | // the GET request has a slight delay
26 | await waitForElement(() => getByText(/clicked 5 times/i));
27 | });
28 |
29 | it('increments count', async () => {
30 | // Render new instance in every test to prevent leaking state
31 | const { getByText } = renderComponent({ count: 5 });
32 |
33 | // It takes time for the button to appear because
34 | // the GET request has a slight delay
35 | await waitForElement(() => getByText('+1'));
36 | fireEvent.click(getByText('+1'));
37 |
38 | // The counter doesn't update immediately because
39 | // the POST request is asynchronous
40 | await waitForElement(() => getByText(/clicked 6 times/i));
41 | });
42 |
--------------------------------------------------------------------------------
/tests/jest-rtl/localstorage/test.js:
--------------------------------------------------------------------------------
1 | // highlight{10-12}
2 | import React from 'react';
3 | import { render, fireEvent, waitForElement } from 'react-testing-library';
4 | import { LocalStorageMock } from '@react-mock/localstorage';
5 | import { PersistentForm } from 'shared/components/PersistentForm';
6 |
7 | // Hoist helper functions (but not vars) to reuse between test cases
8 | const renderComponent = ({ name }) =>
9 | render(
10 |
11 |
12 |
13 | );
14 |
15 | const submitForm = ({ getByText, getByLabelText }, { name }) => {
16 | fireEvent.change(getByLabelText('Name'), { target: { value: name } });
17 | fireEvent.click(getByText(/change name/i));
18 | };
19 |
20 | it('renders cached name', async () => {
21 | // Render new instance in every test to prevent leaking state
22 | const { getByText } = renderComponent({ name: 'Trent' });
23 |
24 | await waitForElement(() => getByText(/welcome, Trent/i));
25 | });
26 |
27 | describe('on update', () => {
28 | it('renders updated name', async () => {
29 | // Render new instance in every test to prevent leaking state
30 | const utils = renderComponent({ name: 'Trent' });
31 | submitForm(utils, { name: 'Trevor' });
32 |
33 | await waitForElement(() => utils.getByText(/welcome, Trevor/i));
34 | });
35 |
36 | it('updates LocalStorage cache', () => {
37 | // Render new instance in every test to prevent leaking state
38 | const utils = renderComponent({ name: 'Trent' });
39 | submitForm(utils, { name: 'Trevor' });
40 |
41 | expect(localStorage.getItem('name')).toBe('Trevor');
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/tests/jest-interactor/fetch/test.js:
--------------------------------------------------------------------------------
1 | // highlight{22,26,30-32,37,43-45}
2 | import React from 'react';
3 | import { mount } from '@bigtest/react';
4 | import Counter from '../counter-interactor';
5 | import { FetchMock } from '@react-mock/fetch';
6 | import { ServerCounter } from 'shared/components/ServerFetchCounter';
7 |
8 | // Hoist helper functions (but not vars) to reuse between test cases
9 | const renderComponent = ({ count }) =>
10 | mount(() => (
11 |
17 |
18 |
19 | ));
20 |
21 | // reuse the custom interactor for the same type of component
22 | let counter = new Counter();
23 |
24 | it('renders initial count', async () => {
25 | // Render new instance in every test to prevent leaking state
26 | await renderComponent({ count: 5 });
27 |
28 | // It takes time for the counter to appear because
29 | // the GET request has a slight delay
30 | await counter.when(() =>
31 | expect(counter.clickedText).toContain('Clicked 5 times')
32 | );
33 | });
34 |
35 | it('increments count', async () => {
36 | // Render new instance in every test to prevent leaking state
37 | await renderComponent({ count: 5 });
38 |
39 | // It takes time for the button to appear because
40 | // the GET request has a slight delay.
41 | // Interactions from interactor are chainable,
42 | // we can increment and then assert in the same chain.
43 | await counter
44 | .increment()
45 | .when(() => expect(counter.clickedText).toContain('Clicked 6 times'));
46 | });
47 |
--------------------------------------------------------------------------------
/tests/jest-rtl/xhr/test.js:
--------------------------------------------------------------------------------
1 | // highlight{15-22,38-41}
2 | import React from 'react';
3 | import { render, fireEvent, waitForElement } from 'react-testing-library';
4 | import { XhrMock } from '@react-mock/xhr';
5 | import { ServerCounter } from 'shared/components/ServerXhrCounter';
6 |
7 | // Hoist helper functions (but not vars) to reuse between test cases
8 | const getRes = count => async (req, res) => res.status(200).body({ count });
9 |
10 | const postRes = count => (req, res) =>
11 | res.status(200).body({ count: count + 1 });
12 |
13 | const renderComponent = ({ count }) =>
14 | render(
15 |
21 |
22 |
23 | );
24 |
25 | it('renders initial count', async () => {
26 | // Render new instance in every test to prevent leaking state
27 | const { getByText } = renderComponent({ count: 5 });
28 |
29 | // It takes time for the counter to appear because
30 | // the GET request has a slight delay
31 | await waitForElement(() => getByText(/clicked 5 times/i));
32 | });
33 |
34 | it('increments count', async () => {
35 | // Render new instance in every test to prevent leaking state
36 | const { getByText } = renderComponent({ count: 5 });
37 |
38 | // It takes time for the button to appear because
39 | // the GET request has a slight delay
40 | await waitForElement(() => getByText('+1'));
41 | fireEvent.click(getByText('+1'));
42 |
43 | // The counter doesn't update immediately because
44 | // the POST request is asynchronous
45 | await waitForElement(() => getByText(/clicked 6 times/i));
46 | });
47 |
--------------------------------------------------------------------------------
/ui/components/shared/styles.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import styled from 'styled-components';
4 |
5 | export const MOBILE_BREAKPOINT = 666;
6 |
7 | export const Center = styled.div`
8 | margin: 0 auto;
9 | max-width: 720px;
10 | box-sizing: border-box;
11 | `;
12 |
13 | export const CenterText = styled(Center)`
14 | padding: 0 24px;
15 | `;
16 |
17 | export const Button = styled.button`
18 | box-sizing: border-box;
19 | border: 0;
20 | background: none;
21 | color: #20232a;
22 | cursor: pointer;
23 | outline: none;
24 | `;
25 |
26 | export const H2 = styled.h2`
27 | margin: 32px 0 24px 0;
28 | font-size: 28px;
29 | font-weight: 300;
30 | line-height: 36px;
31 | opacity: 0.8;
32 | `;
33 |
34 | export const H3 = styled.h3`
35 | margin: 24px 0 16px 0;
36 | font-size: 20px;
37 | font-weight: 500;
38 | line-height: 24px;
39 | `;
40 |
41 | export const Paragraph = styled.p`
42 | margin: 0 0 16px 0;
43 | line-height: 28px;
44 | `;
45 |
46 | export const Blockquote = styled.blockquote`
47 | margin: 0 0 16px 0;
48 | padding: 0 0 0 16px;
49 | border-left: 4px solid #dde0e8;
50 |
51 | p {
52 | color: rgba(32, 35, 42, 0.7);
53 | }
54 | `;
55 |
56 | export const InlineCode = styled.code`
57 | padding: 3px 6px;
58 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
59 | monospace;
60 | font-size: 14px;
61 | background-color: rgba(221, 224, 232, 0.7);
62 | border-radius: 3px;
63 | `;
64 |
65 | export const List = styled.ul`
66 | margin: 8px 0 16px 0;
67 | padding-left: 48px;
68 | `;
69 |
70 | export const ListItem = styled.li`
71 | line-height: 30px;
72 | margin: 4px 0;
73 |
74 | :first-child {
75 | margin-top: 0;
76 | }
77 | :last-child {
78 | margin-bottom: 0;
79 | }
80 | `;
81 |
--------------------------------------------------------------------------------
/ui/components/Header/TestKindSelect.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { Component } from 'react';
4 | import Router from 'next/router';
5 | import styled from 'styled-components';
6 | import svgChevronDown from '../../svg/triangle-down.svg';
7 | import { TEST_KIND_LABELS } from '../../shared/testKinds';
8 |
9 | import type { TTestKindId } from '../../types';
10 |
11 | type Props = {
12 | value: TTestKindId
13 | };
14 |
15 | export class TestKindSelect extends Component {
16 | handleChange = (e: SyntheticInputEvent) => {
17 | const { value } = e.currentTarget;
18 |
19 | Router.push(`/index?testKindId=${value}`, `/${value}`);
20 | };
21 |
22 | render() {
23 | const { value } = this.props;
24 |
25 | return (
26 |
27 | {TEST_KIND_LABELS[value]}
28 |
29 | {Object.keys(TEST_KIND_LABELS).map(testKindId => (
30 |
31 | {TEST_KIND_LABELS[testKindId]}
32 |
33 | ))}
34 |
35 |
36 | );
37 | }
38 | }
39 |
40 | const SelectContainer = styled.div`
41 | position: relative;
42 | padding-right: 16px;
43 | height: 24px;
44 | background: url(${svgChevronDown});
45 | background-size: 10px;
46 | background-position: calc(100%) 6px;
47 | background-repeat: no-repeat;
48 | line-height: 24px;
49 | white-space: nowrap;
50 |
51 | :hover {
52 | text-decoration: underline;
53 | }
54 | `;
55 |
56 | const Select = styled.select`
57 | position: absolute;
58 | top: 0;
59 | left: 0;
60 | width: 100%;
61 | height: 100%;
62 | border: 0;
63 | border-radius: 0;
64 | background: transparent;
65 | line-height: 24px;
66 | white-space: nowrap;
67 | cursor: pointer;
68 | appearance: none;
69 | opacity: 0;
70 | `;
71 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/fetch/test.js:
--------------------------------------------------------------------------------
1 | // highlight{12-19,23-25,34-37,45-49}
2 | import React from 'react';
3 | import { mount } from 'enzyme';
4 | import until from 'async-until';
5 | import retry from '@skidding/async-retry';
6 | import { FetchMock } from '@react-mock/fetch';
7 | import { ServerCounter } from 'shared/components/ServerFetchCounter';
8 |
9 | // Hoist helper functions (but not vars) to reuse between test cases
10 | const getWrapper = ({ count }) =>
11 | mount(
12 |
18 |
19 |
20 | );
21 |
22 | const isReady = wrapper => () => {
23 | // Enzyme wrapper is not updated automatically since v3
24 | // https://github.com/airbnb/enzyme/issues/1163
25 | wrapper.update();
26 |
27 | return !wrapper.text().match(/syncing.../i);
28 | };
29 |
30 | it('renders initial count', async () => {
31 | // Render new instance in every test to prevent leaking state
32 | const wrapper = getWrapper({ count: 5 });
33 |
34 | // It takes time for the counter to appear because
35 | // the GET request has a slight delay
36 | await retry(() => {
37 | expect(wrapper.text()).toMatch(/clicked 5 times/i);
38 | });
39 | });
40 |
41 | it('increments count', async () => {
42 | // Render new instance in every test to prevent leaking state
43 | const wrapper = getWrapper({ count: 5 });
44 |
45 | // It takes time for the button to appear because
46 | // the GET request has a slight delay
47 | await until(isReady(wrapper));
48 |
49 | wrapper.find('button').simulate('click');
50 |
51 | // The counter doesn't update immediately because
52 | // the POST request is asynchronous
53 | await retry(() => {
54 | expect(wrapper.text()).toMatch(/clicked 6 times/i);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/ui/server/start-dev.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { join } from 'path';
4 | import http from 'http';
5 | import express from 'express';
6 | import next from 'next';
7 |
8 | const app = createApp();
9 | const server = http.createServer(app);
10 |
11 | startNextApp(app, server);
12 |
13 | export function createApp(): express$Application {
14 | const app = express();
15 | // This is where Express middlewares can be added
16 | // app.use(...);
17 |
18 | return app;
19 | }
20 |
21 | export async function startNextApp(
22 | app: express$Application,
23 | server: net$Server
24 | ) {
25 | const dev = process.env.NODE_ENV !== 'production';
26 | const nextApp = next({ dev, dir: join(__dirname, '..') });
27 | const nextHandler = nextApp.getRequestHandler();
28 |
29 | await nextApp.prepare();
30 |
31 | app.get('/about', (req: express$Request, res: express$Response) => {
32 | return nextApp.render(req, res, '/about');
33 | });
34 |
35 | app.get(
36 | '/:testKindId/:sectionName',
37 | (req: express$Request, res: express$Response) => {
38 | const { testKindId, sectionName } = req.params;
39 |
40 | return nextApp.render(req, res, '/index', {
41 | testKindId,
42 | sectionName
43 | });
44 | }
45 | );
46 |
47 | app.get('/:testKindId', (req: express$Request, res: express$Response) => {
48 | return nextApp.render(req, res, '/index', {
49 | testKindId: req.params.testKindId
50 | });
51 | });
52 |
53 | app.get('*', (req: express$Request, res: express$Response) => {
54 | return nextHandler(req, res);
55 | });
56 |
57 | startServer(server, 3000);
58 | }
59 |
60 | export function startServer(server: net$Server, port: number) {
61 | // https://github.com/facebook/flow/issues/1684#issuecomment-222627634
62 | server.listen(port, undefined, undefined, err => {
63 | if (err) throw err;
64 |
65 | console.log(`> Ready on http://localhost:${port}`);
66 | });
67 | }
68 |
--------------------------------------------------------------------------------
/ui/components/File/Code/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react';
4 | import styled from 'styled-components';
5 | import { extractCodeLines } from './shared';
6 | import { StyledCode, PrismStyledCode } from './style';
7 |
8 | type Props = {
9 | code: string,
10 | showComments: boolean,
11 | showImports: boolean
12 | };
13 |
14 | export function Code({ code, showComments, showImports }: Props) {
15 | const lines = extractCodeLines({ code, showComments, showImports });
16 | const visibleLines = lines.filter(l => !l.isHidden);
17 |
18 | return (
19 |
20 |
21 |
22 | {visibleLines.map((line, idx) => (
23 |
24 | {line.rawCode}
25 |
26 | ))}
27 |
28 |
29 |
30 | l.coloredCode).join('\n')
33 | }}
34 | />
35 |
36 |
37 | );
38 | }
39 |
40 | const Container = styled.div`
41 | position: relative;
42 | overflow: auto;
43 | `;
44 |
45 | const LineHighlights = styled.pre`
46 | background: #282c34;
47 | border-radius: 10px;
48 | margin: 0;
49 | padding: 16px 0;
50 | overflow: auto;
51 | color: transparent;
52 | `;
53 |
54 | const LineHighlight = styled.div`
55 | min-height: 24px; /* Required for empty lines to take space */
56 | padding: 0 24px;
57 | background: ${props => (props.highlight ? '#14161a' : 'transparent')};
58 |
59 | :last-child {
60 | min-height: auto;
61 | }
62 | `;
63 |
64 | const ColoredCode = styled.pre`
65 | /* Stay on top of the highlights */
66 | position: absolute;
67 | top: 0;
68 |
69 | margin: 0;
70 | padding: 16px 24px;
71 | background: transparent;
72 | color: #ffffff;
73 | `;
74 |
--------------------------------------------------------------------------------
/tests/jest-interactor/xhr/test.js:
--------------------------------------------------------------------------------
1 | // highlight{27,31,35-37,42,50-52}
2 | import React from 'react';
3 | import { mount } from '@bigtest/react';
4 | import Counter from '../counter-interactor';
5 | import { XhrMock } from '@react-mock/xhr';
6 | import { ServerCounter } from 'shared/components/ServerXhrCounter';
7 |
8 | // Hoist helper functions (but not vars) to reuse between test cases
9 | const getRes = count => async (req, res) => res.status(200).body({ count });
10 |
11 | const postRes = count => (req, res) =>
12 | res.status(200).body({ count: count + 1 });
13 |
14 | const renderComponent = ({ count }) =>
15 | mount(() => (
16 |
22 |
23 |
24 | ));
25 |
26 | // reuse the custom interactor for the same type of component
27 | let counter = new Counter();
28 |
29 | it('renders initial count', async () => {
30 | // Render new instance in every test to prevent leaking state
31 | await renderComponent({ count: 5 });
32 |
33 | // It takes time for the counter to appear because
34 | // the GET request has a slight delay
35 | await counter.when(() =>
36 | expect(counter.clickedText).toContain('Clicked 5 times')
37 | );
38 | });
39 |
40 | it('increments count', async () => {
41 | // Render new instance in every test to prevent leaking state
42 | await renderComponent({ count: 5 });
43 |
44 | // It takes time for the button to appear because
45 | // the GET request has a slight delay
46 | // The counter doesn't update immediately because
47 | // the POST request is asynchronous
48 | // Interactions from interactor are chainable,
49 | // we can increment and then assert in the same chain.
50 | await counter
51 | .increment()
52 | .when(() => expect(counter.clickedText).toContain('Clicked 6 times'));
53 | });
54 |
--------------------------------------------------------------------------------
/ui/components/Header/SearchBox.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /* eslint-env browser */
3 |
4 | import React, { Component } from 'react';
5 | import styled from 'styled-components';
6 | import { WindowKeyListener, KEY_S, KEY_ESC } from '../shared/WindowKeyListener';
7 |
8 | type Props = {
9 | searchText: string,
10 | onChange: (searchText: string) => mixed
11 | };
12 |
13 | export class SearchBox extends Component {
14 | searchInput: ?HTMLInputElement;
15 |
16 | handleChange = (e: SyntheticInputEvent) => {
17 | this.props.onChange(e.currentTarget.value);
18 | };
19 |
20 | handleKeyDown = (e: SyntheticKeyboardEvent) => {
21 | const { searchInput } = this;
22 | if (!searchInput) {
23 | return;
24 | }
25 |
26 | const isFocused = searchInput === document.activeElement;
27 | if (e.keyCode === KEY_S && !isFocused) {
28 | // Prevent entering `s` in the search field along with focusing
29 | e.preventDefault();
30 | searchInput.focus();
31 | } else if (e.keyCode === KEY_ESC && isFocused) {
32 | this.props.onChange('');
33 | searchInput.blur();
34 | }
35 | };
36 |
37 | render() {
38 | return (
39 |
40 | {
42 | this.searchInput = node;
43 | }}
44 | type="text"
45 | placeholder="press 's' to search"
46 | value={this.props.searchText}
47 | onChange={this.handleChange}
48 | />
49 |
50 | );
51 | }
52 | }
53 |
54 | const SearchInput = styled.input`
55 | box-sizing: border-box;
56 | width: 272px;
57 | height: 32px;
58 | padding: 4px 16px;
59 | border: 0;
60 | border-radius: 5px;
61 | background: #dde0e8;
62 | color: #20232a;
63 | line-height: 24px;
64 | outline: none;
65 |
66 | ::placeholder {
67 | text-align: center;
68 | color: rgba(32, 35, 42, 0.5);
69 | }
70 | `;
71 |
--------------------------------------------------------------------------------
/ui/pages/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { Component } from 'react';
4 | import Head from 'next/head';
5 | import { App } from '../components/App';
6 | import { gitRef } from '../import-files';
7 | import { getSectionByName } from '../shared/section';
8 | import { TEST_KIND_LABELS, getTestKind } from '../shared/testKinds';
9 |
10 | import type { TTestKindId } from '../types';
11 |
12 | type Query = {
13 | testKindId: TTestKindId,
14 | sectionName?: string
15 | };
16 |
17 | type Props = {
18 | testKindId: TTestKindId,
19 | sectionName?: string
20 | };
21 |
22 | export default class Page extends Component {
23 | static async getInitialProps({ query }: { query: Query }): Promise {
24 | const { testKindId, sectionName } = query;
25 |
26 | return {
27 | testKindId,
28 | sectionName
29 | };
30 | }
31 |
32 | render() {
33 | const { testKindId, sectionName } = this.props;
34 |
35 | return (
36 | <>
37 |
38 | {getPageTitle(testKindId, sectionName)}
39 |
40 |
45 | >
46 | );
47 | }
48 | }
49 |
50 | function getPageTitle(testKindId, sectionName) {
51 | const testKind = getTestKind(testKindId);
52 |
53 | return sectionName
54 | ? getSectionPageTitle(testKind, sectionName)
55 | : getTestKindPageTitle(testKind);
56 | }
57 |
58 | function getSectionPageTitle(testKind, sectionName) {
59 | const { setup, tests } = testKind;
60 | const section = getSectionByName([setup, ...tests], sectionName);
61 |
62 | return `${getSectionTitle(section)} — ${getTestKindPageTitle(testKind)}`;
63 | }
64 |
65 | function getTestKindPageTitle(testKind) {
66 | const testKindLabel = TEST_KIND_LABELS[testKind.id];
67 |
68 | return `${testKindLabel} — React Testing Examples`;
69 | }
70 |
71 | function getSectionTitle(section) {
72 | return section.readme.meta.title;
73 | }
74 |
--------------------------------------------------------------------------------
/tests/jest-enzyme/xhr/test.js:
--------------------------------------------------------------------------------
1 | // highlight{17-24,28-30,39-43,50-54}
2 | import React from 'react';
3 | import { mount } from 'enzyme';
4 | import until from 'async-until';
5 | import retry from '@skidding/async-retry';
6 | import { XhrMock } from '@react-mock/xhr';
7 | import { ServerCounter } from 'shared/components/ServerXhrCounter';
8 |
9 | // Hoist helper functions (but not vars) to reuse between test cases
10 | const getRes = count => async (req, res) => res.status(200).body({ count });
11 |
12 | const postRes = count => (req, res) =>
13 | res.status(200).body({ count: count + 1 });
14 |
15 | const getWrapper = ({ count }) =>
16 | mount(
17 |
23 |
24 |
25 | );
26 |
27 | const isReady = wrapper => () => {
28 | // Enzyme wrapper is not updated automatically since v3
29 | // https://github.com/airbnb/enzyme/issues/1163
30 | wrapper.update();
31 |
32 | return !wrapper.text().match(/syncing.../i);
33 | };
34 |
35 | it('renders initial count', async () => {
36 | // Render new instance in every test to prevent leaking state
37 | const wrapper = getWrapper({ count: 5 });
38 |
39 | // It takes time for the counter to appear because
40 | // the GET request has a slight delay
41 | await retry(() => {
42 | expect(wrapper.text()).toMatch(/clicked 5 times/i);
43 | });
44 | });
45 |
46 | it('increments count', async () => {
47 | // Render new instance in every test to prevent leaking state
48 | const wrapper = getWrapper({ count: 5 });
49 |
50 | // It takes time for the button to appear because
51 | // the GET request has a slight delay
52 | await until(isReady(wrapper));
53 |
54 | wrapper.find('button').simulate('click');
55 |
56 | // The counter doesn't update immediately because
57 | // the POST request is asynchronous
58 | await retry(() => {
59 | expect(wrapper.text()).toMatch(/clicked 6 times/i);
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/ui/components/File/Code/style.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import styled from 'styled-components';
4 |
5 | export const StyledCode = styled.code`
6 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
7 | monospace !important;
8 | font-smooth: always;
9 | -webkit-font-smoothing: antialiased;
10 | -moz-osx-font-smoothing: grayscale;
11 |
12 | height: auto !important;
13 | margin: 0;
14 | font-size: 14px;
15 | line-height: 24px;
16 | white-space: pre-wrap;
17 | word-break: break-word;
18 | `;
19 |
20 | // Styles copied from
21 | // https://github.com/reactjs/reactjs.org/blob/942e83ef396199f499830792b1c61a9c6c990f29/src/prism-styles.js
22 | export const PrismStyledCode = styled(StyledCode)`
23 | .token.attr-name {
24 | color: #c5a5c5;
25 | }
26 | .token.comment,
27 | .token.block-comment,
28 | .token.prolog,
29 | .token.doctype,
30 | .token.cdata {
31 | color: #999999;
32 | }
33 | .token.property,
34 | .token.number,
35 | .token.function-name,
36 | .token.constant,
37 | .token.symbol,
38 | .token.deleted {
39 | color: #5a9bcf;
40 | }
41 | .token.boolean {
42 | color: #ff8b50;
43 | }
44 | .token.tag {
45 | color: #fc929e;
46 | }
47 | .token.string {
48 | color: #8dc891;
49 | }
50 | .token.punctuation {
51 | color: #5fb3b3;
52 | }
53 | .token.selector,
54 | .token.char,
55 | .token.builtin,
56 | .token.inserted {
57 | color: #d8dee9;
58 | }
59 | .token.function {
60 | color: #79b6f2;
61 | }
62 | .token.operator,
63 | .token.entity,
64 | .token.url,
65 | .token.variable {
66 | color: #d7deea;
67 | }
68 | .token.attr-value {
69 | color: #8dc891;
70 | }
71 | .token.keyword {
72 | color: #c5a5c5;
73 | }
74 | .token.atrule,
75 | .token.class-name {
76 | color: #fac863;
77 | }
78 | .token.important {
79 | font-weight: 400;
80 | }
81 | .token.bold {
82 | font-weight: 700;
83 | }
84 | .token.italic {
85 | font-style: italic;
86 | }
87 | .token.entity {
88 | cursor: help;
89 | }
90 | .namespace {
91 | opacity: 0.7;
92 | }
93 | `;
94 |
--------------------------------------------------------------------------------
/tests/jest-interactor/localstorage/test.js:
--------------------------------------------------------------------------------
1 | // highlight{20-28,30,34,36,42,46-49,54,59-65}
2 | import React from 'react';
3 | import { mount } from '@bigtest/react';
4 | import { interactor, text, clickable, fillable } from '@bigtest/interactor';
5 | import { LocalStorageMock } from '@react-mock/localstorage';
6 | import { PersistentForm } from 'shared/components/PersistentForm';
7 |
8 | // Hoist helper functions (but not vars) to reuse between test cases
9 | const renderComponent = ({ name }) =>
10 | mount(() => (
11 |
12 |
13 |
14 | ));
15 |
16 | // Create a custom reusable interactor for this form component.
17 | // These custom interactors aren't usually in your test files
18 | // and are reusable. So you write it once, import, and use it everywhere
19 | // this component is used.
20 | @interactor
21 | class FormInteractor {
22 | // Get the text of the `p` element
23 | welcomeText = text('p');
24 | // Fill in the `#name-field` input with the passed value
25 | fillName = fillable('#name-field');
26 | // Click the submit `button`
27 | submit = clickable('button');
28 | }
29 |
30 | let form = new FormInteractor();
31 |
32 | it('renders cached name', async () => {
33 | // Render new instance in every test to prevent leaking state
34 | await renderComponent({ name: 'Trent' });
35 |
36 | await form.when(() => expect(form.welcomeText).toEqual('Welcome, Trent!'));
37 | });
38 |
39 | describe('on update', () => {
40 | it('renders updated name', async () => {
41 | // Render new instance in every test to prevent leaking state
42 | await renderComponent({ name: 'Trent' });
43 |
44 | // Fill the inputs name in with `Trevor`, submit the form,
45 | // and assert the change has taken place on the page
46 | await form
47 | .fillName('Trevor')
48 | .submit()
49 | .when(() => expect(form.welcomeText).toEqual('Welcome, Trevor!'));
50 | });
51 |
52 | it('updates LocalStorage cache', async () => {
53 | // Render new instance in every test to prevent leaking state
54 | await renderComponent({ name: 'Trent' });
55 |
56 | // Fill the inputs name in with `Bill`, submit the form,
57 | // and assert the change has taken place on the page
58 | // (and in local storage)
59 | await form
60 | .fillName('Bill')
61 | .submit()
62 | .when(() => {
63 | expect(form.welcomeText).toEqual('Welcome, Bill!');
64 | expect(localStorage.getItem('name')).toBe('Bill');
65 | });
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/ui/components/File/Code/shared.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import Prism from 'prismjs';
4 | import { parseCode } from '../shared';
5 |
6 | type ExtractCodeLineArgs = {
7 | code: string,
8 | showComments: boolean,
9 | showImports: boolean
10 | };
11 |
12 | type CodeLine = {
13 | rawCode: string,
14 | coloredCode: string,
15 | isHidden: boolean,
16 | isHighlighted: boolean
17 | };
18 |
19 | export function extractCodeLines({
20 | code: rawCode,
21 | showComments,
22 | showImports
23 | }: ExtractCodeLineArgs): CodeLine[] {
24 | const { lineNumsToHighlight, cleanCode: code } = parseCode(rawCode);
25 |
26 | const coloredCode = Prism.highlight(code, Prism.languages.jsx, 'jsx');
27 | const coloredLines = coloredCode.split('\n');
28 |
29 | const lines = code.split('\n').map((line, lineIndex) => ({
30 | rawCode: line,
31 | coloredCode: coloredLines[lineIndex],
32 | isHidden: isLineHidden({ code: line, showComments, showImports }),
33 | isHighlighted: isLineHighlighted({ lineIndex, lineNumsToHighlight })
34 | }));
35 |
36 | const firstVisibleLine = getFirstVisibleLine(lines);
37 |
38 | // Ensure the visible code doesn't start with an empty line. This can happen
39 | // when imports are hidden
40 | return isLineEmpty(firstVisibleLine)
41 | ? hideLine(lines, firstVisibleLine)
42 | : lines;
43 | }
44 |
45 | function getFirstVisibleLine(lines) {
46 | return lines.filter(l => !l.isHidden)[0];
47 | }
48 |
49 | function isLineEmpty(line) {
50 | return line && line.rawCode === '';
51 | }
52 |
53 | function hideLine(lines, line) {
54 | const lineIndex = lines.indexOf(line);
55 |
56 | return [
57 | ...lines.slice(0, lineIndex),
58 | { ...line, isHidden: true },
59 | ...lines.slice(lineIndex + 1)
60 | ];
61 | }
62 |
63 | function isLineHighlighted({ lineIndex, lineNumsToHighlight }) {
64 | // +1 line because lines start from 0 programatically, and +1 because we
65 | // remove the first line with the highlight{...} comment
66 | return lineNumsToHighlight.indexOf(lineIndex + 2) !== -1;
67 | }
68 |
69 | function isLineHidden({ code, showComments, showImports }) {
70 | return (
71 | (!showComments && isCommentLine(code)) ||
72 | (!showImports && isNoSideEffectsImportLine(code))
73 | );
74 | }
75 |
76 | function isNoSideEffectsImportLine(code) {
77 | // NOTE: Imported modules without imports are omitted
78 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Import_a_module_for_its_side_effects_only
79 | return Boolean(code.match(/^import [^']+/));
80 | }
81 |
82 | function isCommentLine(code) {
83 | return Boolean(code.match(/^\s*\/\//));
84 | }
85 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | ## Second version
2 |
3 | > Started on Oct 16, 2018
4 |
5 | - [x] Use react-mock in tests
6 | - [x] Remove hoisted vars and before/after cases
7 | - [x] Refactor code preview component
8 | - [x] Try react-testing-library
9 | - [x] Split tests between RTL and Enzyme
10 | - [x] Remove Cosmos fixture tests (superseded by react-mock)
11 | - [x] Refactor styled-components test
12 | - [x] Add URL for each test kind
13 | - [x] Add URL for each test
14 | - [x] Update project description
15 | - [x] Update README
16 | - [x] Visual facelift
17 | - [x] Header
18 | - [x] Remove file visibility toggling
19 | - [x] Tweak file actions
20 | - [x] Footer
21 | - [x] Links to tech used
22 | - [x] Update responsive breakpoints
23 | - [x] Replace file system sorting
24 | - [x] Make Next.js components load in Cosmos
25 | - [ ] Optimize search perf
26 |
27 | ## First version
28 |
29 | > Started on Apr 24, 2018
30 |
31 | - [x] Add "Click callback" tests
32 | - [x] Add "Render text" tests
33 | - [x] Add "Local state" tests
34 | - [x] Add "Redux" tests
35 | - [x] Add "React Router" tests
36 | - [x] Add "XHR" tests
37 | - [x] Add "Fetch" tests
38 | - [x] Add "LocalStorage" tests
39 | - [x] Add "styled-components" tests
40 | - [ ] Add "Context" tests
41 | - [x] Colocate components with tests
42 | - [x] Add ESLint
43 | - [x] Config CircleCI
44 | - [x] Colocate Cosmos proxies with tests
45 | - [x] Add Flow
46 | - [x] Create UI
47 | - [x] Syntax highlight (with line highlight)
48 | - [x] Sticky header
49 | - [x] Highlight and create fixtures for all tests
50 | - [x] Implement search
51 | - [x] Add toggle between 'Plain Enzyme' and 'Cosmos & Enzyme'
52 | - [x] Improve search
53 | - [x] Keyboard shortcuts
54 | - [x] No results screen
55 | - [x] Show best matches first
56 | - [x] Add links next to section titles
57 | - [x] Add file actions to copy and open code in GH
58 | - [x] Create About modal
59 | - [x] Footer
60 | - [x] Design no results screen
61 | - [x] Adapt header design on mobile
62 | - [x] Tweak colors
63 | - [x] Load test title & description from README pages
64 | - [x] Write copy
65 | - [x] Info overlay
66 | - [x] Opinionated test style (integration / abstract libs)
67 | - [x] Cosmos vs non-Cosmos
68 | - [x] Test READMEs
69 | - [x] Create build script
70 | - [x] Add Next.js
71 | - [x] Reconcile Babel config between Next, Cosmos and Jest
72 | - [x] Reconcile webpack config between Next and Cosmos
73 | - [x] Restructure files for more clarity
74 | - [x] Read tests from disk at compile time
75 | - [x] Point to latest commit SHA
76 | - [x] PUBLISH
77 | - [x] Create fixtures for UI components
78 | - [ ] Config Playground for tests
79 |
--------------------------------------------------------------------------------
/ui/components/AboutModal.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { Component } from 'react';
4 | import Router from 'next/router';
5 | import Link from 'next/link';
6 | import styled from 'styled-components';
7 | import WhatsThis from '../../WHATSTHIS.md';
8 | import { CenterText, H2, H3, Button, Paragraph } from './shared/styles';
9 | import { WindowKeyListener, KEY_ESC } from './shared/WindowKeyListener';
10 |
11 | type Props = {};
12 |
13 | export class AboutModal extends Component {
14 | handleContentClick = (e: SyntheticEvent) => {
15 | e.stopPropagation();
16 | };
17 |
18 | handleKeyDown = (e: SyntheticKeyboardEvent) => {
19 | if (e.keyCode === KEY_ESC) {
20 | goHome();
21 | }
22 | };
23 |
24 | render() {
25 | return (
26 |
27 |
28 |
29 | ,
35 | ol: OpinionsList
36 | }}
37 | />
38 |
39 |
40 | Show me some tests
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | function goHome() {
51 | Router.push('/');
52 | }
53 |
54 | const Overlay = styled.div`
55 | position: fixed;
56 | top: 0;
57 | bottom: 0;
58 | left: 0;
59 | right: 0;
60 | z-index: 2;
61 | padding: 32px 12px;
62 | background: rgba(221, 224, 232, 0.9);
63 | overflow-x: hidden;
64 | overflow-y: auto;
65 | `;
66 |
67 | const Content = styled(CenterText)`
68 | background: #fff;
69 | border-radius: 10px;
70 | box-shadow: 0 3px 15px 0 rgba(32, 35, 42, 0.2);
71 | overflow: hidden;
72 | padding: 8px 24px 16px 24px;
73 |
74 | @media (min-width: 480px) {
75 | padding: 8px 36px 16px 36px;
76 | }
77 | `;
78 |
79 | const ButtonContainer = styled.div`
80 | text-align: right;
81 | `;
82 |
83 | const OpinionsList = styled.ol`
84 | padding-left: 36px;
85 | `;
86 |
87 | const SubtleLink = styled.a`
88 | color: #20232a;
89 | font-weight: 500;
90 | `;
91 |
92 | const GoButton = styled(Button)`
93 | display: inline-block;
94 | padding: 12px 20px;
95 | border-radius: 5px;
96 | background: #2b51ad;
97 | color: #f5f7f9;
98 | line-height: 24px;
99 | `;
100 |
--------------------------------------------------------------------------------
/ui/webpack-loaders/import-tests-loader.js:
--------------------------------------------------------------------------------
1 | const { join } = require('path');
2 | const glob = require('glob');
3 | const { execSync } = require('child_process');
4 | const { getTestKindIds, getTestNames } = require('../server/testFiles');
5 |
6 | const TESTS_PATH = join(__dirname, `../../tests`);
7 |
8 | module.exports = function parseReadme(source) {
9 | const testKindsStr = getTestKindIds().map(testKindId =>
10 | getTestKindStr(testKindId)
11 | );
12 |
13 | const gitRef = getLastCommit();
14 |
15 | // Re-build webpack bundle on test file changes
16 | this.addContextDependency(TESTS_PATH);
17 |
18 | return source
19 | .replace(
20 | 'testKinds: TTestKinds = {}',
21 | `testKinds: TTestKinds = {
22 | ${testKindsStr.join(`,\n `)}
23 | }`
24 | )
25 | .replace(`gitRef: string = ''`, `gitRef = '${gitRef}'`);
26 | };
27 |
28 | function getTestKindStr(testKindId) {
29 | return `'${testKindId}': {
30 | id: '${testKindId}',
31 | order: require('${getOrderPath(testKindId)}').default,
32 | setup: ${getSetupStr(testKindId)},
33 | tests: [${getTestNames(testKindId)
34 | .map(testName => getTestStr(testKindId, testName))
35 | .join(`, `)}]
36 | }`;
37 | }
38 |
39 | function getSetupStr(testKindId) {
40 | const readmePath = getSetupPath(testKindId, 'README.md');
41 |
42 | return getSectionStr({
43 | name: 'setup',
44 | readmePath,
45 | files: glob
46 | .sync(`*.js`, { cwd: getTestKindRootPath(testKindId) })
47 | .filter(p => ['order.js'].indexOf(p) === -1)
48 | .map(p => getSetupPath(testKindId, p))
49 | });
50 | }
51 |
52 | function getTestStr(testKindId, testName) {
53 | const readmePath = getTestFilePath(testKindId, testName, 'README.md');
54 | const testPath = getTestFilePath(testKindId, testName, 'test.js');
55 |
56 | return getSectionStr({
57 | name: testName,
58 | readmePath,
59 | files: [testPath]
60 | });
61 | }
62 |
63 | function getSectionStr({ name, readmePath, files }) {
64 | return `{
65 | name: '${name}',
66 | readme: ${getReadmeStr({ readmePath })},
67 | files: {
68 | ${files.map(filePath => getFileStr({ filePath })).join(`,\n `)}
69 | }
70 | }`;
71 | }
72 |
73 | function getReadmeStr({ readmePath }) {
74 | const readmeTextLoader = getLoaderPath('readme-text-loader');
75 |
76 | return `{
77 | meta: require('!${readmeTextLoader}!${readmePath}'),
78 | component: require('${readmePath}').default
79 | }`;
80 | }
81 |
82 | function getFileStr({ filePath }) {
83 | const fileName = filePath.split('/').pop();
84 |
85 | return `'${fileName}': require('!raw-loader!${filePath}')`;
86 | }
87 |
88 | function getSetupPath(testKindId, filePath) {
89 | return join(getTestKindRootPath(testKindId), filePath);
90 | }
91 |
92 | function getTestFilePath(testKindId, testName, filePath) {
93 | return join(getTestKindRootPath(testKindId), testName, filePath);
94 | }
95 |
96 | function getOrderPath(testKindId) {
97 | return join(getTestKindRootPath(testKindId), 'order.js');
98 | }
99 |
100 | function getTestKindRootPath(testKindId) {
101 | return join(TESTS_PATH, testKindId);
102 | }
103 |
104 | function getLoaderPath(filePath) {
105 | return join(__dirname, `../webpack-loaders/${filePath}`);
106 | }
107 |
108 | function getLastCommit() {
109 | // Long live StackOverflow!
110 | return execSync(`git log | head -n 1 | awk '{print $2}'`)
111 | .toString()
112 | .trim();
113 | }
114 |
--------------------------------------------------------------------------------
/ui/components/File/FileActions.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { Component } from 'react';
4 | import styled from 'styled-components';
5 | import Clipboard from 'clipboard';
6 | import { GitRef } from '../../contexts';
7 | import svgClippy from '../../svg/clippy.svg';
8 | import svgLinkExternal from '../../svg/link-external.svg';
9 | import { Button } from '../shared/styles';
10 | import { parseCode } from './shared';
11 |
12 | const { now } = Date;
13 |
14 | type Props = {
15 | filePath: string,
16 | code: string
17 | };
18 |
19 | type CopyStatus = null | 'success' | 'error';
20 |
21 | type State = {
22 | copyStatus: CopyStatus,
23 | copyTime: number
24 | };
25 |
26 | export class FileActions extends Component {
27 | clipboard: ?typeof Clipboard;
28 |
29 | state = {
30 | copyStatus: null,
31 | copyTime: 0
32 | };
33 |
34 | componentWillUnmount() {
35 | if (this.clipboard) {
36 | this.clipboard.destroy();
37 | }
38 | }
39 |
40 | handleCopyBtnRef = (node: ?HTMLElement) => {
41 | if (node) {
42 | const clipboard = new Clipboard(node);
43 | clipboard.on('success', this.handleCopySuccess);
44 | clipboard.on('error', this.handleCopyError);
45 |
46 | this.clipboard = clipboard;
47 | } else {
48 | this.clipboard = null;
49 | }
50 | };
51 |
52 | handleCopySuccess = () => {
53 | this.setState({
54 | copyStatus: 'success',
55 | copyTime: now()
56 | });
57 | };
58 |
59 | handleCopyError = () => {
60 | this.setState({
61 | copyStatus: 'error',
62 | copyTime: now()
63 | });
64 | };
65 |
66 | render() {
67 | const { filePath, code } = this.props;
68 | const { copyStatus, copyTime } = this.state;
69 | const { cleanCode } = parseCode(code);
70 |
71 | return (
72 |
73 | {gitRef => (
74 |
75 |
83 |
90 |
91 | )}
92 |
93 | );
94 | }
95 | }
96 |
97 | const PROJECT_ROOT_URL = 'https://github.com/skidding/react-testing-examples';
98 |
99 | function getFileUrl(gitRef: string, filePath: string) {
100 | return `${PROJECT_ROOT_URL}/blob/${gitRef}/tests/${filePath}`;
101 | }
102 |
103 | const Container = styled.div`
104 | display: flex;
105 | `;
106 |
107 | const Action = styled(Button)`
108 | width: 36px;
109 | height: 36px;
110 | padding: 0 8px;
111 | background: url(${props => props.icon});
112 | background-size: 20px;
113 | background-position: center 6px;
114 | background-repeat: no-repeat;
115 | border-radius: 3px;
116 | opacity: 0.5;
117 | transition: opacity 0.3s;
118 |
119 | :hover {
120 | opacity: 0.9;
121 | }
122 | `;
123 |
124 | const CopyAction = styled(Action)`
125 | animation: flash${props => props.time} 3s;
126 |
127 | @keyframes flash${props => props.time} {
128 | from {
129 | background-color: ${props => getCopyBgColorByStatus(props.status)};
130 | }
131 | to {
132 | background-color: transparent;
133 | }
134 | }
135 | `;
136 |
137 | function getCopyBgColorByStatus(status: CopyStatus) {
138 | switch (status) {
139 | case 'success':
140 | return '#64e88d';
141 | case 'error':
142 | return '#f14342';
143 | default:
144 | return 'transparent';
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "lint": "eslint '**/*.js'",
4 | "test:jest-enzyme": "jest --config tests/jest-enzyme/jest.config.js",
5 | "test:jest-enzyme:watch": "yarn test:jest-enzyme --watch",
6 | "test:jest-rtl": "jest --config tests/jest-rtl/jest.config.js",
7 | "test:jest-rtl:watch": "yarn test:jest-rtl --watch",
8 | "test:jest-interactor": "jest --config tests/jest-interactor/jest.config.js",
9 | "test:jest-interactor:watch": "yarn test:jest-interactor --watch",
10 | "test:ui": "yarn jest --config ui/jest.config.js",
11 | "test:ui:watch": "yarn test:ui --watch",
12 | "test": "yarn test:jest-enzyme && yarn test:jest-rtl && yarn test:jest-interactor && yarn test:ui",
13 | "test:ci": "yarn test:jest-enzyme --maxWorkers=2 && yarn test:jest-rtl --maxWorkers=2 && yarn test:jest-interactor --maxWorkers=2 && yarn test:ui --maxWorkers=2",
14 | "cosmos": "cosmos-classic --config ui/cosmos.config",
15 | "dev": "yarn babel-node ui/server/start-dev.js",
16 | "build": "next build ui && next export ui -o .export",
17 | "upload": "cd .export && now --name rte --public && cd -"
18 | },
19 | "devDependencies": {
20 | "@babel/cli": "7.1.2",
21 | "@babel/core": "7.1.2",
22 | "@babel/node": "^7.0.0",
23 | "@babel/plugin-proposal-class-properties": "7.1.0",
24 | "@babel/plugin-proposal-decorators": "^7.1.6",
25 | "@babel/plugin-proposal-object-rest-spread": "7.0.0",
26 | "@babel/preset-env": "7.1.0",
27 | "@babel/preset-flow": "7.0.0",
28 | "@babel/preset-react": "7.0.0",
29 | "@bigtest/interactor": "^0.9.1",
30 | "@bigtest/react": "^0.1.2",
31 | "@mdx-js/loader": "^0.15.5",
32 | "@mdx-js/mdx": "^0.15.5",
33 | "@react-mock/fetch": "^0.3.0",
34 | "@react-mock/localstorage": "^0.1.2",
35 | "@react-mock/state": "^0.1.7",
36 | "@react-mock/xhr": "^0.2.0",
37 | "@skidding/async-retry": "^1.0.2",
38 | "async-until": "^1.2.2",
39 | "axios": "^0.18.0",
40 | "babel-core": "^7.0.0-bridge.0",
41 | "babel-eslint": "^10.0.1",
42 | "babel-jest": "^23.6.0",
43 | "babel-loader": "^8.0.4",
44 | "babel-plugin-inline-import-data-uri": "^1.0.1",
45 | "babel-plugin-module-resolver": "^3.1.1",
46 | "babel-plugin-styled-components": "^1.8.0",
47 | "css-loader": "^1.0.0",
48 | "enzyme": "^3.7.0",
49 | "enzyme-adapter-react-16": "^1.6.0",
50 | "eslint": "^5.7.0",
51 | "eslint-import-resolver-babel-module": "^5.0.0-beta.1",
52 | "eslint-plugin-babel": "^5.2.1",
53 | "eslint-plugin-flowtype": "^3.0.0",
54 | "eslint-plugin-import": "^2.14.0",
55 | "eslint-plugin-jest": "^21.25.1",
56 | "eslint-plugin-react": "^7.11.1",
57 | "express": "^4.16.4",
58 | "flow-bin": "^0.93.0",
59 | "flow-typed": "^2.5.1",
60 | "glob": "^7.1.3",
61 | "html-webpack-plugin": "^3.2.0",
62 | "jest": "^24.1.0",
63 | "jest-dom": "^2.1.0",
64 | "prettier": "^1.16.4",
65 | "raw-loader": "^0.5.1",
66 | "react-cosmos-classic": "^4.8.3",
67 | "react-cosmos-fetch-proxy": "^4.8.2",
68 | "react-cosmos-localstorage-proxy": "^4.8.2",
69 | "react-cosmos-redux-proxy": "^4.8.2",
70 | "react-cosmos-router-proxy": "^4.8.2",
71 | "react-cosmos-test": "^4.8.2",
72 | "react-cosmos-xhr-proxy": "^4.8.2",
73 | "react-redux": "^5.0.7",
74 | "react-router": "^4.3.1",
75 | "react-router-dom": "^4.3.1",
76 | "react-test-renderer": "^16.5.2",
77 | "react-testing-library": "^5.2.0",
78 | "redux": "^4.0.1",
79 | "remark-parse": "^5.0.0",
80 | "style-loader": "^0.23.1",
81 | "traverse": "^0.6.6",
82 | "unified": "^7.0.0",
83 | "webpack": "^4.21.0"
84 | },
85 | "dependencies": {
86 | "clipboard": "^2.0.4",
87 | "delay": "^4.1.0",
88 | "fuzzaldrin-plus": "^0.6.0",
89 | "next": "^7.0.2",
90 | "parse-numeric-range": "^0.0.2",
91 | "prismjs": "^1.15.0",
92 | "query-string": "^6.2.0",
93 | "react": "^16.8.3",
94 | "react-dom": "^16.8.3",
95 | "react-show": "2.0.4",
96 | "styled-components": "^4.1.3"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/ui/components/SectionList/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { Component } from 'react';
4 | import styled from 'styled-components';
5 | import { shouldSearch } from '../../search';
6 | import svgInfo from '../../svg/info.svg';
7 | import { SectionLink } from '../shared/SectionLink';
8 | import { FuzzyHighlighter } from '../shared/FuzzyHighlighter';
9 | import { hasSectionChanged } from '../../shared/section';
10 | import { CenterText, Paragraph, List, ListItem } from '../shared/styles';
11 | import thinkin from './img/thinkin.png';
12 | import { ToggleShow } from './ToggleShow';
13 | import { ToggleButton } from './ToggleButton';
14 |
15 | import type { TTestKindId, TSection } from '../../types';
16 |
17 | type Props = {
18 | sections: TSection[],
19 | testKindId: TTestKindId,
20 | sectionName: ?string,
21 | searchText: string,
22 | changeSearch: (searchText: string) => mixed
23 | };
24 |
25 | type State = {
26 | isOpen: boolean
27 | };
28 |
29 | export class SectionList extends Component {
30 | state = {
31 | isOpen: false
32 | };
33 |
34 | componentDidUpdate(prevProps: Props) {
35 | if (this.state.isOpen && hasSectionChanged(this.props, prevProps)) {
36 | this.setState({
37 | isOpen: false
38 | });
39 | }
40 | }
41 |
42 | handleToggleList = () => {
43 | this.setState({ isOpen: !this.state.isOpen });
44 | };
45 |
46 | handleClearSearch = (e: SyntheticEvent) => {
47 | e.preventDefault();
48 | this.props.changeSearch('');
49 | };
50 |
51 | render() {
52 | const { searchText } = this.props;
53 | const { isOpen } = this.state;
54 |
55 | if (shouldSearch(searchText)) {
56 | return (
57 |
58 | {this.renderSearchTitle()}
59 | {this.renderContent()}
60 |
61 | );
62 | }
63 |
64 | return (
65 |
66 | (
68 |
73 | )}
74 | content={this.renderContent()}
75 | show={isOpen}
76 | onToggle={this.handleToggleList}
77 | />
78 |
79 | );
80 | }
81 |
82 | renderSearchTitle() {
83 | const { sections, searchText } = this.props;
84 |
85 | if (!sections.length) {
86 | return (
87 |
88 | No results found for "{searchText}" {this.renderClearSearchBtn()}
89 |
90 | );
91 | }
92 |
93 | return (
94 |
95 | Results for "{searchText}" {this.renderClearSearchBtn()}
96 |
97 | );
98 | }
99 |
100 | renderContent() {
101 | const { sections, testKindId, sectionName, searchText } = this.props;
102 |
103 | if (!sections.length) {
104 | return (
105 | <>
106 |
107 |
108 |
109 |
110 | Contact{' '}
111 |
112 | Ovidiu
113 | {' '}
114 | if you need help testing React components
115 |
116 |
117 | >
118 | );
119 | }
120 |
121 | return (
122 |
123 | {sections.map(section => {
124 | const { name, readme } = section;
125 | const { title } = readme.meta;
126 | const hlText = (
127 |
128 | );
129 |
130 | return (
131 |
132 | {sectionName === name ? (
133 | {hlText}
134 | ) : (
135 |
136 | {hlText}
137 |
138 | )}
139 |
140 | );
141 | })}
142 |
143 | );
144 | }
145 |
146 | renderClearSearchBtn() {
147 | return (
148 |
149 | (
150 |
151 | clear search
152 |
153 | )
154 |
155 | );
156 | }
157 | }
158 |
159 | const Container = styled(CenterText)`
160 | margin-top: 8px;
161 | `;
162 |
163 | const CustomList = styled(List)`
164 | margin: 0;
165 | padding-top: 0;
166 | padding-left: 24px;
167 | `;
168 |
169 | const SelectedItem = styled.span`
170 | color: #888e9c;
171 | font-weight: 500;
172 | `;
173 |
174 | const SearchHeader = styled.p`
175 | margin: 0;
176 | padding: 8px 0;
177 | line-height: 24px;
178 | `;
179 |
180 | const ClearSearchBtn = styled.span`
181 | opacity: 0.7;
182 |
183 | a {
184 | color: #20232a;
185 | white-space: nowrap;
186 | }
187 | `;
188 |
189 | const ThinkinFace = styled.div`
190 | margin: 32px auto;
191 | width: 140px;
192 | height: 140px;
193 | background: #f5f7f9 url(${thinkin}) no-repeat center center;
194 | background-size: 140px 140px;
195 | background-blend-mode: luminosity;
196 | opacity: 0;
197 | filter: blur(16px);
198 | animation: fadeIn 1s forwards;
199 |
200 | @keyframes fadeIn {
201 | from {
202 | opacity: 0;
203 | filter: blur(16px);
204 | }
205 | to {
206 | opacity: 1;
207 | filter: blur(0);
208 | }
209 | }
210 | `;
211 |
212 | const ContactParagraph = styled(Paragraph)`
213 | display: flex;
214 | flex-direction: row;
215 | justify-content: center;
216 | margin: 24px 0;
217 |
218 | .icon {
219 | display: inline-block;
220 | width: 24px;
221 | height: 24px;
222 | margin-right: 4px;
223 | background: url(${svgInfo});
224 | background-size: 20px;
225 | background-position: center center;
226 | background-repeat: no-repeat;
227 | opacity: 0.5;
228 | flex-shrink: 0;
229 | }
230 |
231 | .text {
232 | color: rgba(32, 35, 42, 0.7);
233 | }
234 | `;
235 |
--------------------------------------------------------------------------------
/ui/components/Header/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { Component } from 'react';
4 | import styled from 'styled-components';
5 | import svgSettings from '../../svg/settings.svg';
6 | import svgChevronLeft from '../../svg/chevron-left.svg';
7 | import { MOBILE_BREAKPOINT, Center, Button } from '../shared/styles';
8 | import { SectionLink } from '../shared/SectionLink';
9 | import { TestKindSelect } from './TestKindSelect';
10 | import { GithubLink } from './GithubLink';
11 | import { AboutButton } from './AboutButton';
12 | import { SearchBox } from './SearchBox';
13 | import { CommentsCheckbox, ImportsCheckbox } from './Checkbox';
14 |
15 | import type { TTestKindId } from '../../types';
16 |
17 | type Props = {
18 | testKindId: TTestKindId,
19 | toggleComments: () => mixed,
20 | toggleImports: () => mixed,
21 | searchText: string,
22 | changeSearch: (searchText: string) => mixed
23 | };
24 |
25 | type State = {
26 | mobileShowFilters: boolean
27 | };
28 |
29 | export class Header extends Component {
30 | state = {
31 | mobileShowFilters: false
32 | };
33 |
34 | handleMobileShowFilters = () => {
35 | this.setState({ mobileShowFilters: true });
36 | };
37 |
38 | handleMobileHideFilters = () => {
39 | this.setState({ mobileShowFilters: false });
40 | };
41 |
42 | render() {
43 | const {
44 | testKindId,
45 | toggleComments,
46 | toggleImports,
47 | searchText,
48 | changeSearch
49 | } = this.props;
50 | const { mobileShowFilters } = this.state;
51 |
52 | return (
53 |
54 |
55 |
56 |
61 |
66 |
67 |
72 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | {' '}
85 | {' '}
86 |
87 |
88 |
89 |
90 | );
91 | }
92 | }
93 |
94 | const Container = styled.div`
95 | position: fixed;
96 | top: 0;
97 | width: 100%;
98 | min-width: 320px;
99 | height: 96px;
100 | padding: 8px 12px 4px 12px;
101 | box-sizing: border-box;
102 | background: #fff;
103 | box-shadow: 0 2px 0px 0px rgba(32, 35, 42, 0.15);
104 | color: #20232a;
105 | overflow: hidden;
106 | z-index: 1;
107 |
108 | @media (max-width: ${MOBILE_BREAKPOINT}px) {
109 | height: 80px;
110 | padding: 2px 12px 0 12px;
111 | }
112 | `;
113 |
114 | const Inner = styled(Center)`
115 | position: relative;
116 | height: 100%;
117 | `;
118 |
119 | const Left = styled.div`
120 | position: absolute;
121 | top: 0;
122 | bottom: 0;
123 | left: 0;
124 |
125 | h1 {
126 | position: absolute;
127 | top: 0;
128 | left: 0;
129 | color: rgb(32, 35, 42);
130 | line-height: 48px;
131 | font-size: 26px;
132 | font-weight: 700;
133 | font-style: italic;
134 | text-transform: uppercase;
135 | letter-spacing: 1px;
136 | white-space: nowrap;
137 | opacity: 0.8;
138 |
139 | a {
140 | font-weight: inherit;
141 | color: #20232a;
142 | text-decoration: none;
143 | }
144 | }
145 |
146 | .actions {
147 | display: flex;
148 | position: absolute;
149 | bottom: 8px;
150 | left: 0;
151 | height: 24px;
152 | line-height: 24px;
153 | }
154 |
155 | @media (max-width: ${MOBILE_BREAKPOINT}px) {
156 | h1 {
157 | font-size: 22px;
158 | }
159 | }
160 | `;
161 |
162 | const Right = styled.div`
163 | display: flex;
164 | flex-direction: column;
165 | justify-content: space-between;
166 | position: absolute;
167 | top: 0;
168 | bottom: 0;
169 | right: 0;
170 |
171 | .search {
172 | padding: 8px 0 0 8px;
173 | right: 0;
174 | }
175 |
176 | .toggles {
177 | display: flex;
178 | justify-content: center;
179 | height: 24px;
180 | padding: 0 0 8px 8px;
181 | line-height: 24px;
182 | text-align: center;
183 | }
184 |
185 | @media (max-width: ${MOBILE_BREAKPOINT}px) {
186 | .search {
187 | display: ${props => (props.mobileShowFilters ? 'block' : 'none')};
188 | }
189 |
190 | .toggles {
191 | display: ${props => (props.mobileShowFilters ? 'flex' : 'none')};
192 | }
193 | }
194 | `;
195 |
196 | const MobileShowFilters = styled(Button)`
197 | display: none;
198 | position: absolute;
199 | top: 0;
200 | right: 0;
201 | width: 40px;
202 | height: 48px;
203 | background: rgba(255, 255, 255, 0.9);
204 | background-image: url(${svgSettings});
205 | background-size: 28px;
206 | background-position: center center;
207 | background-repeat: no-repeat;
208 | opacity: 0.9;
209 |
210 | @media (max-width: ${MOBILE_BREAKPOINT}px) {
211 | display: ${props => (props.filtersVisible ? 'none' : 'block')};
212 | }
213 | `;
214 |
215 | const MobileHideFilters = styled(Button)`
216 | display: none;
217 | position: absolute;
218 | top: 0;
219 | left: 0;
220 | width: 100%;
221 | height: 100%;
222 | background: linear-gradient(
223 | to left,
224 | #fff,
225 | #fff 216px,
226 | rgba(255, 255, 255, 0.85)
227 | );
228 |
229 | .icon {
230 | position: absolute;
231 | top: 0;
232 | left: 0;
233 | width: 56px;
234 | height: 80px;
235 | background-image: url(${svgChevronLeft});
236 | background-size: 32px;
237 | background-position: center center;
238 | background-repeat: no-repeat;
239 | opacity: 0.9;
240 | }
241 |
242 | @media (max-width: ${MOBILE_BREAKPOINT}px) {
243 | display: ${props => (props.filtersVisible ? 'block' : 'none')};
244 | }
245 | `;
246 |
--------------------------------------------------------------------------------
/ui/components/App.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /* eslint-env browser */
3 |
4 | import React, { Component } from 'react';
5 | import styled from 'styled-components';
6 | import { sortBy } from 'lodash';
7 | import { FileOptions, GitRef } from '../contexts';
8 | import { shouldSearch, matchReadmeText, sortSections } from '../search';
9 | import { hasSectionChanged, getSectionByName } from '../shared/section';
10 | import { MOBILE_BREAKPOINT } from './shared/styles';
11 | import { Header } from './Header';
12 | import { AboutModal } from './AboutModal';
13 | import { SectionList } from './SectionList';
14 | import { Section } from './Section';
15 | import { Footer } from './Footer';
16 |
17 | import type { TTestKind, TSection } from '../types';
18 |
19 | type Props = {
20 | gitRef: string,
21 | testKind: TTestKind,
22 | sectionName?: string,
23 | showAbout: boolean
24 | };
25 |
26 | type State = {
27 | showComments: boolean,
28 | showImports: boolean,
29 | searchText: string
30 | };
31 |
32 | export class App extends Component {
33 | static defaultProps = {
34 | showAbout: false
35 | };
36 |
37 | state = {
38 | showAboutModal: false,
39 | showComments: true,
40 | showImports: false,
41 | searchText: ''
42 | };
43 |
44 | handleToggleComments = () => {
45 | this.setState({ showComments: !this.state.showComments });
46 | };
47 |
48 | handleToggleImports = () => {
49 | this.setState({ showImports: !this.state.showImports });
50 | };
51 |
52 | handleSearchChange = (searchText: string) => {
53 | this.setState({ searchText });
54 | };
55 |
56 | componentDidMount() {
57 | setBodyScroll(this.props.showAbout);
58 | }
59 |
60 | componentDidUpdate(prevProps: Props, prevState: State) {
61 | const { showAbout } = this.props;
62 | const { searchText } = this.state;
63 |
64 | if (
65 | searchText &&
66 | hasSectionChanged(getSectionProps(this.props), getSectionProps(prevProps))
67 | ) {
68 | this.setState({
69 | searchText: ''
70 | });
71 | }
72 | // Jump to top when changing search query, because results will change
73 | // anyway so previous scroll position will be irrelevant
74 | else if (searchText && searchText !== prevState.searchText) {
75 | window.scrollTo(0, 0);
76 | }
77 |
78 | if (showAbout !== prevProps.showAbout) {
79 | setBodyScroll(showAbout);
80 | }
81 | }
82 |
83 | render() {
84 | const { gitRef, testKind, sectionName, showAbout } = this.props;
85 | const { showComments, showImports, searchText } = this.state;
86 |
87 | const isSearching = shouldSearch(searchText);
88 | const sections = [testKind.setup, ...getSortedTests(testKind)];
89 |
90 | return (
91 |
92 |
93 |
94 |
95 |
102 |
103 | {isSearching ? (
104 | this.renderSearchContent(sections)
105 | ) : (
106 | <>
107 |
114 | {sectionName
115 | ? getSectionEl({
116 | section: getSectionByName(sections, sectionName),
117 | testKind,
118 | searchText
119 | })
120 | : sections.map(section =>
121 | getSectionEl({ section, testKind, searchText })
122 | )}
123 | >
124 | )}
125 |
126 |
127 | {showAbout && }
128 |
129 |
130 |
131 | );
132 | }
133 |
134 | renderSearchContent(sections: TSection[]) {
135 | const { testKind, sectionName } = this.props;
136 | const { searchText } = this.state;
137 |
138 | const matchingSections = sortSections(
139 | sections.filter(
140 | s => matchReadmeText(s.readme.meta, searchText),
141 | searchText
142 | ),
143 | searchText
144 | );
145 |
146 | return (
147 | <>
148 |
155 | {matchingSections.map(section =>
156 | getSectionEl({ section, testKind, searchText })
157 | )}
158 | >
159 | );
160 | }
161 | }
162 |
163 | function getSortedTests(testKind) {
164 | const { tests, order } = testKind;
165 |
166 | return sortBy(tests, test => {
167 | const idx = order.indexOf(test.name);
168 |
169 | // Show tests without an explicit position last
170 | return idx === -1 ? Infinity : idx;
171 | });
172 | }
173 |
174 | function getSectionEl({ section, testKind, searchText }) {
175 | return (
176 |
182 | );
183 | }
184 |
185 | function getSectionProps({ testKind, sectionName }) {
186 | return {
187 | testKindId: testKind.id,
188 | sectionName
189 | };
190 | }
191 |
192 | function setBodyScroll(hasModal: boolean) {
193 | // Prevent double scroll when modal is open
194 | if (document.body) {
195 | document.body.className = hasModal ? 'with-modal' : '';
196 | }
197 | }
198 |
199 | const TopSpace = styled.div`
200 | height: 96px;
201 |
202 | @media (max-width: ${MOBILE_BREAKPOINT}px) {
203 | height: 80px;
204 | }
205 | `;
206 |
207 | const Content = styled.div`
208 | background: #f5f7f9;
209 | color: #20232a;
210 | `;
211 |
212 | const ContentCenter = styled.div`
213 | margin: 0 auto;
214 | box-sizing: border-box;
215 | padding: 2px 12px 32px 12px;
216 | min-width: 320px;
217 | max-width: 1476px;
218 | overflow: hidden;
219 | `;
220 |
--------------------------------------------------------------------------------
/flow-typed/npm/express_v4.16.x.js:
--------------------------------------------------------------------------------
1 | // flow-typed signature: 164dcf1c9105e51cb17a374a807146a7
2 | // flow-typed version: c7f4cf7a4d/express_v4.16.x/flow_>=v0.93.x
3 |
4 | import * as http from "http";
5 | import type { Socket } from "net";
6 |
7 | declare type express$RouterOptions = {
8 | caseSensitive?: boolean,
9 | mergeParams?: boolean,
10 | strict?: boolean
11 | };
12 |
13 | declare class express$RequestResponseBase {
14 | app: express$Application;
15 | get(field: string): string | void;
16 | }
17 |
18 | declare type express$RequestParams = {
19 | [param: string]: string
20 | };
21 |
22 | declare class express$Request extends http$IncomingMessage mixins express$RequestResponseBase {
23 | baseUrl: string;
24 | body: mixed;
25 | cookies: { [cookie: string]: string };
26 | connection: Socket;
27 | fresh: boolean;
28 | hostname: string;
29 | ip: string;
30 | ips: Array;
31 | method: string;
32 | originalUrl: string;
33 | params: express$RequestParams;
34 | path: string;
35 | protocol: "https" | "http";
36 | query: { [name: string]: string | Array };
37 | route: string;
38 | secure: boolean;
39 | signedCookies: { [signedCookie: string]: string };
40 | stale: boolean;
41 | subdomains: Array;
42 | xhr: boolean;
43 | accepts(types: string): string | false;
44 | accepts(types: Array): string | false;
45 | acceptsCharsets(...charsets: Array): string | false;
46 | acceptsEncodings(...encoding: Array): string | false;
47 | acceptsLanguages(...lang: Array): string | false;
48 | header(field: string): string | void;
49 | is(type: string): boolean;
50 | param(name: string, defaultValue?: string): string | void;
51 | }
52 |
53 | declare type express$CookieOptions = {
54 | domain?: string,
55 | encode?: (value: string) => string,
56 | expires?: Date,
57 | httpOnly?: boolean,
58 | maxAge?: number,
59 | path?: string,
60 | secure?: boolean,
61 | signed?: boolean
62 | };
63 |
64 | declare type express$Path = string | RegExp;
65 |
66 | declare type express$RenderCallback = (
67 | err: Error | null,
68 | html?: string
69 | ) => mixed;
70 |
71 | declare type express$SendFileOptions = {
72 | maxAge?: number,
73 | root?: string,
74 | lastModified?: boolean,
75 | headers?: { [name: string]: string },
76 | dotfiles?: "allow" | "deny" | "ignore"
77 | };
78 |
79 | declare class express$Response extends http$ServerResponse mixins express$RequestResponseBase {
80 | headersSent: boolean;
81 | locals: { [name: string]: mixed };
82 | append(field: string, value?: string): this;
83 | attachment(filename?: string): this;
84 | cookie(name: string, value: string, options?: express$CookieOptions): this;
85 | clearCookie(name: string, options?: express$CookieOptions): this;
86 | download(
87 | path: string,
88 | filename?: string,
89 | callback?: (err?: ?Error) => void
90 | ): this;
91 | format(typesObject: { [type: string]: Function }): this;
92 | json(body?: mixed): this;
93 | jsonp(body?: mixed): this;
94 | links(links: { [name: string]: string }): this;
95 | location(path: string): this;
96 | redirect(url: string, ...args: Array): this;
97 | redirect(status: number, url: string, ...args: Array): this;
98 | render(
99 | view: string,
100 | locals?: { [name: string]: mixed },
101 | callback?: express$RenderCallback
102 | ): this;
103 | send(body?: mixed): this;
104 | sendFile(
105 | path: string,
106 | options?: express$SendFileOptions,
107 | callback?: (err?: ?Error) => mixed
108 | ): this;
109 | sendStatus(statusCode: number): this;
110 | header(field: string, value?: string): this;
111 | header(headers: { [name: string]: string }): this;
112 | set(field: string, value?: string | string[]): this;
113 | set(headers: { [name: string]: string }): this;
114 | status(statusCode: number): this;
115 | type(type: string): this;
116 | vary(field: string): this;
117 | req: express$Request;
118 | }
119 |
120 | declare type express$NextFunction = (err?: ?Error | "route") => mixed;
121 | declare type express$Middleware =
122 | | ((
123 | req: $Subtype,
124 | res: express$Response,
125 | next: express$NextFunction
126 | ) => mixed)
127 | | ((
128 | error: Error,
129 | req: $Subtype,
130 | res: express$Response,
131 | next: express$NextFunction
132 | ) => mixed);
133 | declare interface express$RouteMethodType {
134 | (middleware: express$Middleware): T;
135 | (...middleware: Array): T;
136 | (
137 | path: express$Path | express$Path[],
138 | ...middleware: Array
139 | ): T;
140 | }
141 | declare class express$Route {
142 | all: express$RouteMethodType;
143 | get: express$RouteMethodType;
144 | post: express$RouteMethodType;
145 | put: express$RouteMethodType;
146 | head: express$RouteMethodType;
147 | delete: express$RouteMethodType;
148 | options: express$RouteMethodType;
149 | trace: express$RouteMethodType;
150 | copy: express$RouteMethodType;
151 | lock: express$RouteMethodType;
152 | mkcol: express$RouteMethodType;
153 | move: express$RouteMethodType;
154 | purge: express$RouteMethodType;
155 | propfind: express$RouteMethodType;
156 | proppatch: express$RouteMethodType;
157 | unlock: express$RouteMethodType;
158 | report: express$RouteMethodType;
159 | mkactivity: express$RouteMethodType;
160 | checkout: express$RouteMethodType;
161 | merge: express$RouteMethodType;
162 |
163 | // @TODO Missing 'm-search' but get flow illegal name error.
164 |
165 | notify: express$RouteMethodType;
166 | subscribe: express$RouteMethodType;
167 | unsubscribe: express$RouteMethodType;
168 | patch: express$RouteMethodType;
169 | search: express$RouteMethodType;
170 | connect: express$RouteMethodType;
171 | }
172 |
173 | declare class express$Router extends express$Route {
174 | constructor(options?: express$RouterOptions): void;
175 | route(path: string): express$Route;
176 | static (options?: express$RouterOptions): express$Router;
177 | use(middleware: express$Middleware): this;
178 | use(...middleware: Array): this;
179 | use(
180 | path: express$Path | express$Path[],
181 | ...middleware: Array
182 | ): this;
183 | use(path: string, router: express$Router): this;
184 | handle(
185 | req: http$IncomingMessage<>,
186 | res: http$ServerResponse,
187 | next: express$NextFunction
188 | ): void;
189 | param(
190 | param: string,
191 | callback: (
192 | req: $Subtype,
193 | res: express$Response,
194 | next: express$NextFunction,
195 | id: string
196 | ) => mixed
197 | ): void;
198 | (
199 | req: http$IncomingMessage<>,
200 | res: http$ServerResponse,
201 | next?: ?express$NextFunction
202 | ): void;
203 | }
204 |
205 | /*
206 | With flow-bin ^0.59, express app.listen() is deemed to return any and fails flow type coverage.
207 | Which is ironic because https://github.com/facebook/flow/blob/master/Changelog.md#misc-2 (release notes for 0.59)
208 | says "Improves typings for Node.js HTTP server listen() function." See that? IMPROVES!
209 | To work around this issue, we changed Server to ?Server here, so that our invocations of express.listen() will
210 | not be deemed to lack type coverage.
211 | */
212 |
213 | declare class express$Application extends express$Router mixins events$EventEmitter {
214 | constructor(): void;
215 | locals: { [name: string]: mixed };
216 | mountpath: string;
217 | listen(
218 | port: number,
219 | hostname?: string,
220 | backlog?: number,
221 | callback?: (err?: ?Error) => mixed
222 | ): ?http.Server;
223 | listen(
224 | port: number,
225 | hostname?: string,
226 | callback?: (err?: ?Error) => mixed
227 | ): ?http.Server;
228 | listen(port: number, callback?: (err?: ?Error) => mixed): ?http.Server;
229 | listen(path: string, callback?: (err?: ?Error) => mixed): ?http.Server;
230 | listen(handle: Object, callback?: (err?: ?Error) => mixed): ?http.Server;
231 | disable(name: string): void;
232 | disabled(name: string): boolean;
233 | enable(name: string): express$Application;
234 | enabled(name: string): boolean;
235 | engine(name: string, callback: Function): void;
236 | /**
237 | * Mixed will not be taken as a value option. Issue around using the GET http method name and the get for settings.
238 | */
239 | // get(name: string): mixed;
240 | set(name: string, value: mixed): mixed;
241 | render(
242 | name: string,
243 | optionsOrFunction: { [name: string]: mixed },
244 | callback: express$RenderCallback
245 | ): void;
246 | handle(
247 | req: http$IncomingMessage<>,
248 | res: http$ServerResponse,
249 | next?: ?express$NextFunction
250 | ): void;
251 | // callable signature is not inherited
252 | (
253 | req: http$IncomingMessage<>,
254 | res: http$ServerResponse,
255 | next?: ?express$NextFunction
256 | ): void;
257 | }
258 |
259 | declare type JsonOptions = {
260 | inflate?: boolean,
261 | limit?: string | number,
262 | reviver?: (key: string, value: mixed) => mixed,
263 | strict?: boolean,
264 | type?: string | Array | ((req: express$Request) => boolean),
265 | verify?: (
266 | req: express$Request,
267 | res: express$Response,
268 | buf: Buffer,
269 | encoding: string
270 | ) => mixed
271 | };
272 |
273 | declare type express$UrlEncodedOptions = {
274 | extended?: boolean,
275 | inflate?: boolean,
276 | limit?: string | number,
277 | parameterLimit?: number,
278 | type?: string | Array | ((req: express$Request) => boolean),
279 | verify?: (
280 | req: express$Request,
281 | res: express$Response,
282 | buf: Buffer,
283 | encoding: string
284 | ) => mixed,
285 | }
286 |
287 | declare module "express" {
288 | declare export type RouterOptions = express$RouterOptions;
289 | declare export type CookieOptions = express$CookieOptions;
290 | declare export type Middleware = express$Middleware;
291 | declare export type NextFunction = express$NextFunction;
292 | declare export type RequestParams = express$RequestParams;
293 | declare export type $Response = express$Response;
294 | declare export type $Request = express$Request;
295 | declare export type $Application = express$Application;
296 |
297 | declare module.exports: {
298 | (): express$Application, // If you try to call like a function, it will use this signature
299 | json: (opts: ?JsonOptions) => express$Middleware,
300 | static: (root: string, options?: Object) => express$Middleware, // `static` property on the function
301 | Router: typeof express$Router, // `Router` property on the function
302 | urlencoded: (opts: ?express$UrlEncodedOptions) => express$Middleware,
303 | };
304 | }
305 |
--------------------------------------------------------------------------------
/flow-typed/npm/jest_v23.x.x.js:
--------------------------------------------------------------------------------
1 | // flow-typed signature: 0f67472200deae3d6dc3a941ed8a54b6
2 | // flow-typed version: 393032fc51/jest_v23.x.x/flow_>=v0.39.x
3 |
4 | type JestMockFn, TReturn> = {
5 | (...args: TArguments): TReturn,
6 | /**
7 | * An object for introspecting mock calls
8 | */
9 | mock: {
10 | /**
11 | * An array that represents all calls that have been made into this mock
12 | * function. Each call is represented by an array of arguments that were
13 | * passed during the call.
14 | */
15 | calls: Array,
16 | /**
17 | * An array that contains all the object instances that have been
18 | * instantiated from this mock function.
19 | */
20 | instances: Array,
21 | /**
22 | * An array that contains all the object results that have been
23 | * returned by this mock function call
24 | */
25 | results: Array<{ isThrow: boolean, value: TReturn }>
26 | },
27 | /**
28 | * Resets all information stored in the mockFn.mock.calls and
29 | * mockFn.mock.instances arrays. Often this is useful when you want to clean
30 | * up a mock's usage data between two assertions.
31 | */
32 | mockClear(): void,
33 | /**
34 | * Resets all information stored in the mock. This is useful when you want to
35 | * completely restore a mock back to its initial state.
36 | */
37 | mockReset(): void,
38 | /**
39 | * Removes the mock and restores the initial implementation. This is useful
40 | * when you want to mock functions in certain test cases and restore the
41 | * original implementation in others. Beware that mockFn.mockRestore only
42 | * works when mock was created with jest.spyOn. Thus you have to take care of
43 | * restoration yourself when manually assigning jest.fn().
44 | */
45 | mockRestore(): void,
46 | /**
47 | * Accepts a function that should be used as the implementation of the mock.
48 | * The mock itself will still record all calls that go into and instances
49 | * that come from itself -- the only difference is that the implementation
50 | * will also be executed when the mock is called.
51 | */
52 | mockImplementation(
53 | fn: (...args: TArguments) => TReturn
54 | ): JestMockFn,
55 | /**
56 | * Accepts a function that will be used as an implementation of the mock for
57 | * one call to the mocked function. Can be chained so that multiple function
58 | * calls produce different results.
59 | */
60 | mockImplementationOnce(
61 | fn: (...args: TArguments) => TReturn
62 | ): JestMockFn,
63 | /**
64 | * Accepts a string to use in test result output in place of "jest.fn()" to
65 | * indicate which mock function is being referenced.
66 | */
67 | mockName(name: string): JestMockFn,
68 | /**
69 | * Just a simple sugar function for returning `this`
70 | */
71 | mockReturnThis(): void,
72 | /**
73 | * Accepts a value that will be returned whenever the mock function is called.
74 | */
75 | mockReturnValue(value: TReturn): JestMockFn,
76 | /**
77 | * Sugar for only returning a value once inside your mock
78 | */
79 | mockReturnValueOnce(value: TReturn): JestMockFn,
80 | /**
81 | * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value))
82 | */
83 | mockResolvedValue(value: TReturn): JestMockFn>,
84 | /**
85 | * Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value))
86 | */
87 | mockResolvedValueOnce(value: TReturn): JestMockFn>,
88 | /**
89 | * Sugar for jest.fn().mockImplementation(() => Promise.reject(value))
90 | */
91 | mockRejectedValue(value: TReturn): JestMockFn>,
92 | /**
93 | * Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value))
94 | */
95 | mockRejectedValueOnce(value: TReturn): JestMockFn>
96 | };
97 |
98 | type JestAsymmetricEqualityType = {
99 | /**
100 | * A custom Jasmine equality tester
101 | */
102 | asymmetricMatch(value: mixed): boolean
103 | };
104 |
105 | type JestCallsType = {
106 | allArgs(): mixed,
107 | all(): mixed,
108 | any(): boolean,
109 | count(): number,
110 | first(): mixed,
111 | mostRecent(): mixed,
112 | reset(): void
113 | };
114 |
115 | type JestClockType = {
116 | install(): void,
117 | mockDate(date: Date): void,
118 | tick(milliseconds?: number): void,
119 | uninstall(): void
120 | };
121 |
122 | type JestMatcherResult = {
123 | message?: string | (() => string),
124 | pass: boolean
125 | };
126 |
127 | type JestMatcher = (actual: any, expected: any) =>
128 | | JestMatcherResult
129 | | Promise;
130 |
131 | type JestPromiseType = {
132 | /**
133 | * Use rejects to unwrap the reason of a rejected promise so any other
134 | * matcher can be chained. If the promise is fulfilled the assertion fails.
135 | */
136 | rejects: JestExpectType,
137 | /**
138 | * Use resolves to unwrap the value of a fulfilled promise so any other
139 | * matcher can be chained. If the promise is rejected the assertion fails.
140 | */
141 | resolves: JestExpectType
142 | };
143 |
144 | /**
145 | * Jest allows functions and classes to be used as test names in test() and
146 | * describe()
147 | */
148 | type JestTestName = string | Function;
149 |
150 | /**
151 | * Plugin: jest-styled-components
152 | */
153 |
154 | type JestStyledComponentsMatcherValue =
155 | | string
156 | | JestAsymmetricEqualityType
157 | | RegExp
158 | | typeof undefined;
159 |
160 | type JestStyledComponentsMatcherOptions = {
161 | media?: string;
162 | modifier?: string;
163 | supports?: string;
164 | }
165 |
166 | type JestStyledComponentsMatchersType = {
167 | toHaveStyleRule(
168 | property: string,
169 | value: JestStyledComponentsMatcherValue,
170 | options?: JestStyledComponentsMatcherOptions
171 | ): void,
172 | };
173 |
174 | /**
175 | * Plugin: jest-enzyme
176 | */
177 | type EnzymeMatchersType = {
178 | // 5.x
179 | toBeEmpty(): void,
180 | toBePresent(): void,
181 | // 6.x
182 | toBeChecked(): void,
183 | toBeDisabled(): void,
184 | toBeEmptyRender(): void,
185 | toContainMatchingElement(selector: string): void;
186 | toContainMatchingElements(n: number, selector: string): void;
187 | toContainExactlyOneMatchingElement(selector: string): void;
188 | toContainReact(element: React$Element): void,
189 | toExist(): void,
190 | toHaveClassName(className: string): void,
191 | toHaveHTML(html: string): void,
192 | toHaveProp: ((propKey: string, propValue?: any) => void) & ((props: Object) => void),
193 | toHaveRef(refName: string): void,
194 | toHaveState: ((stateKey: string, stateValue?: any) => void) & ((state: Object) => void),
195 | toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: Object) => void),
196 | toHaveTagName(tagName: string): void,
197 | toHaveText(text: string): void,
198 | toHaveValue(value: any): void,
199 | toIncludeText(text: string): void,
200 | toMatchElement(
201 | element: React$Element,
202 | options?: {| ignoreProps?: boolean, verbose?: boolean |},
203 | ): void,
204 | toMatchSelector(selector: string): void,
205 | // 7.x
206 | toHaveDisplayName(name: string): void,
207 | };
208 |
209 | // DOM testing library extensions https://github.com/kentcdodds/dom-testing-library#custom-jest-matchers
210 | type DomTestingLibraryType = {
211 | toBeInTheDOM(): void,
212 | toHaveTextContent(content: string): void,
213 | toHaveAttribute(name: string, expectedValue?: string): void
214 | };
215 |
216 | // Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers
217 | type JestJQueryMatchersType = {
218 | toExist(): void,
219 | toHaveLength(len: number): void,
220 | toHaveId(id: string): void,
221 | toHaveClass(className: string): void,
222 | toHaveTag(tag: string): void,
223 | toHaveAttr(key: string, val?: any): void,
224 | toHaveProp(key: string, val?: any): void,
225 | toHaveText(text: string | RegExp): void,
226 | toHaveData(key: string, val?: any): void,
227 | toHaveValue(val: any): void,
228 | toHaveCss(css: {[key: string]: any}): void,
229 | toBeChecked(): void,
230 | toBeDisabled(): void,
231 | toBeEmpty(): void,
232 | toBeHidden(): void,
233 | toBeSelected(): void,
234 | toBeVisible(): void,
235 | toBeFocused(): void,
236 | toBeInDom(): void,
237 | toBeMatchedBy(sel: string): void,
238 | toHaveDescendant(sel: string): void,
239 | toHaveDescendantWithText(sel: string, text: string | RegExp): void
240 | };
241 |
242 |
243 | // Jest Extended Matchers: https://github.com/jest-community/jest-extended
244 | type JestExtendedMatchersType = {
245 | /**
246 | * Note: Currently unimplemented
247 | * Passing assertion
248 | *
249 | * @param {String} message
250 | */
251 | // pass(message: string): void;
252 |
253 | /**
254 | * Note: Currently unimplemented
255 | * Failing assertion
256 | *
257 | * @param {String} message
258 | */
259 | // fail(message: string): void;
260 |
261 | /**
262 | * Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty.
263 | */
264 | toBeEmpty(): void;
265 |
266 | /**
267 | * Use .toBeOneOf when checking if a value is a member of a given Array.
268 | * @param {Array.<*>} members
269 | */
270 | toBeOneOf(members: any[]): void;
271 |
272 | /**
273 | * Use `.toBeNil` when checking a value is `null` or `undefined`.
274 | */
275 | toBeNil(): void;
276 |
277 | /**
278 | * Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`.
279 | * @param {Function} predicate
280 | */
281 | toSatisfy(predicate: (n: any) => boolean): void;
282 |
283 | /**
284 | * Use `.toBeArray` when checking if a value is an `Array`.
285 | */
286 | toBeArray(): void;
287 |
288 | /**
289 | * Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x.
290 | * @param {Number} x
291 | */
292 | toBeArrayOfSize(x: number): void;
293 |
294 | /**
295 | * Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set.
296 | * @param {Array.<*>} members
297 | */
298 | toIncludeAllMembers(members: any[]): void;
299 |
300 | /**
301 | * Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set.
302 | * @param {Array.<*>} members
303 | */
304 | toIncludeAnyMembers(members: any[]): void;
305 |
306 | /**
307 | * Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array.
308 | * @param {Function} predicate
309 | */
310 | toSatisfyAll(predicate: (n: any) => boolean): void;
311 |
312 | /**
313 | * Use `.toBeBoolean` when checking if a value is a `Boolean`.
314 | */
315 | toBeBoolean(): void;
316 |
317 | /**
318 | * Use `.toBeTrue` when checking a value is equal (===) to `true`.
319 | */
320 | toBeTrue(): void;
321 |
322 | /**
323 | * Use `.toBeFalse` when checking a value is equal (===) to `false`.
324 | */
325 | toBeFalse(): void;
326 |
327 | /**
328 | * Use .toBeDate when checking if a value is a Date.
329 | */
330 | toBeDate(): void;
331 |
332 | /**
333 | * Use `.toBeFunction` when checking if a value is a `Function`.
334 | */
335 | toBeFunction(): void;
336 |
337 | /**
338 | * Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`.
339 | *
340 | * Note: Required Jest version >22
341 | * Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same
342 | *
343 | * @param {Mock} mock
344 | */
345 | toHaveBeenCalledBefore(mock: JestMockFn): void;
346 |
347 | /**
348 | * Use `.toBeNumber` when checking if a value is a `Number`.
349 | */
350 | toBeNumber(): void;
351 |
352 | /**
353 | * Use `.toBeNaN` when checking a value is `NaN`.
354 | */
355 | toBeNaN(): void;
356 |
357 | /**
358 | * Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`.
359 | */
360 | toBeFinite(): void;
361 |
362 | /**
363 | * Use `.toBePositive` when checking if a value is a positive `Number`.
364 | */
365 | toBePositive(): void;
366 |
367 | /**
368 | * Use `.toBeNegative` when checking if a value is a negative `Number`.
369 | */
370 | toBeNegative(): void;
371 |
372 | /**
373 | * Use `.toBeEven` when checking if a value is an even `Number`.
374 | */
375 | toBeEven(): void;
376 |
377 | /**
378 | * Use `.toBeOdd` when checking if a value is an odd `Number`.
379 | */
380 | toBeOdd(): void;
381 |
382 | /**
383 | * Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive).
384 | *
385 | * @param {Number} start
386 | * @param {Number} end
387 | */
388 | toBeWithin(start: number, end: number): void;
389 |
390 | /**
391 | * Use `.toBeObject` when checking if a value is an `Object`.
392 | */
393 | toBeObject(): void;
394 |
395 | /**
396 | * Use `.toContainKey` when checking if an object contains the provided key.
397 | *
398 | * @param {String} key
399 | */
400 | toContainKey(key: string): void;
401 |
402 | /**
403 | * Use `.toContainKeys` when checking if an object has all of the provided keys.
404 | *
405 | * @param {Array.} keys
406 | */
407 | toContainKeys(keys: string[]): void;
408 |
409 | /**
410 | * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys.
411 | *
412 | * @param {Array.} keys
413 | */
414 | toContainAllKeys(keys: string[]): void;
415 |
416 | /**
417 | * Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys.
418 | *
419 | * @param {Array.} keys
420 | */
421 | toContainAnyKeys(keys: string[]): void;
422 |
423 | /**
424 | * Use `.toContainValue` when checking if an object contains the provided value.
425 | *
426 | * @param {*} value
427 | */
428 | toContainValue(value: any): void;
429 |
430 | /**
431 | * Use `.toContainValues` when checking if an object contains all of the provided values.
432 | *
433 | * @param {Array.<*>} values
434 | */
435 | toContainValues(values: any[]): void;
436 |
437 | /**
438 | * Use `.toContainAllValues` when checking if an object only contains all of the provided values.
439 | *
440 | * @param {Array.<*>} values
441 | */
442 | toContainAllValues(values: any[]): void;
443 |
444 | /**
445 | * Use `.toContainAnyValues` when checking if an object contains at least one of the provided values.
446 | *
447 | * @param {Array.<*>} values
448 | */
449 | toContainAnyValues(values: any[]): void;
450 |
451 | /**
452 | * Use `.toContainEntry` when checking if an object contains the provided entry.
453 | *
454 | * @param {Array.} entry
455 | */
456 | toContainEntry(entry: [string, string]): void;
457 |
458 | /**
459 | * Use `.toContainEntries` when checking if an object contains all of the provided entries.
460 | *
461 | * @param {Array.>} entries
462 | */
463 | toContainEntries(entries: [string, string][]): void;
464 |
465 | /**
466 | * Use `.toContainAllEntries` when checking if an object only contains all of the provided entries.
467 | *
468 | * @param {Array.>} entries
469 | */
470 | toContainAllEntries(entries: [string, string][]): void;
471 |
472 | /**
473 | * Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries.
474 | *
475 | * @param {Array.>} entries
476 | */
477 | toContainAnyEntries(entries: [string, string][]): void;
478 |
479 | /**
480 | * Use `.toBeExtensible` when checking if an object is extensible.
481 | */
482 | toBeExtensible(): void;
483 |
484 | /**
485 | * Use `.toBeFrozen` when checking if an object is frozen.
486 | */
487 | toBeFrozen(): void;
488 |
489 | /**
490 | * Use `.toBeSealed` when checking if an object is sealed.
491 | */
492 | toBeSealed(): void;
493 |
494 | /**
495 | * Use `.toBeString` when checking if a value is a `String`.
496 | */
497 | toBeString(): void;
498 |
499 | /**
500 | * Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings.
501 | *
502 | * @param {String} string
503 | */
504 | toEqualCaseInsensitive(string: string): void;
505 |
506 | /**
507 | * Use `.toStartWith` when checking if a `String` starts with a given `String` prefix.
508 | *
509 | * @param {String} prefix
510 | */
511 | toStartWith(prefix: string): void;
512 |
513 | /**
514 | * Use `.toEndWith` when checking if a `String` ends with a given `String` suffix.
515 | *
516 | * @param {String} suffix
517 | */
518 | toEndWith(suffix: string): void;
519 |
520 | /**
521 | * Use `.toInclude` when checking if a `String` includes the given `String` substring.
522 | *
523 | * @param {String} substring
524 | */
525 | toInclude(substring: string): void;
526 |
527 | /**
528 | * Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times.
529 | *
530 | * @param {String} substring
531 | * @param {Number} times
532 | */
533 | toIncludeRepeated(substring: string, times: number): void;
534 |
535 | /**
536 | * Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings.
537 | *
538 | * @param {Array.} substring
539 | */
540 | toIncludeMultiple(substring: string[]): void;
541 | };
542 |
543 | interface JestExpectType {
544 | not:
545 | & JestExpectType
546 | & EnzymeMatchersType
547 | & DomTestingLibraryType
548 | & JestJQueryMatchersType
549 | & JestStyledComponentsMatchersType
550 | & JestExtendedMatchersType,
551 | /**
552 | * If you have a mock function, you can use .lastCalledWith to test what
553 | * arguments it was last called with.
554 | */
555 | lastCalledWith(...args: Array): void,
556 | /**
557 | * toBe just checks that a value is what you expect. It uses === to check
558 | * strict equality.
559 | */
560 | toBe(value: any): void,
561 | /**
562 | * Use .toBeCalledWith to ensure that a mock function was called with
563 | * specific arguments.
564 | */
565 | toBeCalledWith(...args: Array): void,
566 | /**
567 | * Using exact equality with floating point numbers is a bad idea. Rounding
568 | * means that intuitive things fail.
569 | */
570 | toBeCloseTo(num: number, delta: any): void,
571 | /**
572 | * Use .toBeDefined to check that a variable is not undefined.
573 | */
574 | toBeDefined(): void,
575 | /**
576 | * Use .toBeFalsy when you don't care what a value is, you just want to
577 | * ensure a value is false in a boolean context.
578 | */
579 | toBeFalsy(): void,
580 | /**
581 | * To compare floating point numbers, you can use toBeGreaterThan.
582 | */
583 | toBeGreaterThan(number: number): void,
584 | /**
585 | * To compare floating point numbers, you can use toBeGreaterThanOrEqual.
586 | */
587 | toBeGreaterThanOrEqual(number: number): void,
588 | /**
589 | * To compare floating point numbers, you can use toBeLessThan.
590 | */
591 | toBeLessThan(number: number): void,
592 | /**
593 | * To compare floating point numbers, you can use toBeLessThanOrEqual.
594 | */
595 | toBeLessThanOrEqual(number: number): void,
596 | /**
597 | * Use .toBeInstanceOf(Class) to check that an object is an instance of a
598 | * class.
599 | */
600 | toBeInstanceOf(cls: Class<*>): void,
601 | /**
602 | * .toBeNull() is the same as .toBe(null) but the error messages are a bit
603 | * nicer.
604 | */
605 | toBeNull(): void,
606 | /**
607 | * Use .toBeTruthy when you don't care what a value is, you just want to
608 | * ensure a value is true in a boolean context.
609 | */
610 | toBeTruthy(): void,
611 | /**
612 | * Use .toBeUndefined to check that a variable is undefined.
613 | */
614 | toBeUndefined(): void,
615 | /**
616 | * Use .toContain when you want to check that an item is in a list. For
617 | * testing the items in the list, this uses ===, a strict equality check.
618 | */
619 | toContain(item: any): void,
620 | /**
621 | * Use .toContainEqual when you want to check that an item is in a list. For
622 | * testing the items in the list, this matcher recursively checks the
623 | * equality of all fields, rather than checking for object identity.
624 | */
625 | toContainEqual(item: any): void,
626 | /**
627 | * Use .toEqual when you want to check that two objects have the same value.
628 | * This matcher recursively checks the equality of all fields, rather than
629 | * checking for object identity.
630 | */
631 | toEqual(value: any): void,
632 | /**
633 | * Use .toHaveBeenCalled to ensure that a mock function got called.
634 | */
635 | toHaveBeenCalled(): void,
636 | toBeCalled(): void;
637 | /**
638 | * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact
639 | * number of times.
640 | */
641 | toHaveBeenCalledTimes(number: number): void,
642 | toBeCalledTimes(number: number): void;
643 | /**
644 | *
645 | */
646 | toHaveBeenNthCalledWith(nthCall: number, ...args: Array): void;
647 | nthCalledWith(nthCall: number, ...args: Array): void;
648 | /**
649 | *
650 | */
651 | toHaveReturned(): void;
652 | toReturn(): void;
653 | /**
654 | *
655 | */
656 | toHaveReturnedTimes(number: number): void;
657 | toReturnTimes(number: number): void;
658 | /**
659 | *
660 | */
661 | toHaveReturnedWith(value: any): void;
662 | toReturnWith(value: any): void;
663 | /**
664 | *
665 | */
666 | toHaveLastReturnedWith(value: any): void;
667 | lastReturnedWith(value: any): void;
668 | /**
669 | *
670 | */
671 | toHaveNthReturnedWith(nthCall: number, value: any): void;
672 | nthReturnedWith(nthCall: number, value: any): void;
673 | /**
674 | * Use .toHaveBeenCalledWith to ensure that a mock function was called with
675 | * specific arguments.
676 | */
677 | toHaveBeenCalledWith(...args: Array): void,
678 | toBeCalledWith(...args: Array): void,
679 | /**
680 | * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called
681 | * with specific arguments.
682 | */
683 | toHaveBeenLastCalledWith(...args: Array): void,
684 | lastCalledWith(...args: Array): void,
685 | /**
686 | * Check that an object has a .length property and it is set to a certain
687 | * numeric value.
688 | */
689 | toHaveLength(number: number): void,
690 | /**
691 | *
692 | */
693 | toHaveProperty(propPath: string, value?: any): void,
694 | /**
695 | * Use .toMatch to check that a string matches a regular expression or string.
696 | */
697 | toMatch(regexpOrString: RegExp | string): void,
698 | /**
699 | * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object.
700 | */
701 | toMatchObject(object: Object | Array): void,
702 | /**
703 | * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object.
704 | */
705 | toStrictEqual(value: any): void,
706 | /**
707 | * This ensures that an Object matches the most recent snapshot.
708 | */
709 | toMatchSnapshot(propertyMatchers?: any, name?: string): void,
710 | /**
711 | * This ensures that an Object matches the most recent snapshot.
712 | */
713 | toMatchSnapshot(name: string): void,
714 |
715 | toMatchInlineSnapshot(snapshot?: string): void,
716 | toMatchInlineSnapshot(propertyMatchers?: any, snapshot?: string): void,
717 | /**
718 | * Use .toThrow to test that a function throws when it is called.
719 | * If you want to test that a specific error gets thrown, you can provide an
720 | * argument to toThrow. The argument can be a string for the error message,
721 | * a class for the error, or a regex that should match the error.
722 | *
723 | * Alias: .toThrowError
724 | */
725 | toThrow(message?: string | Error | Class | RegExp): void,
726 | toThrowError(message?: string | Error | Class | RegExp): void,
727 | /**
728 | * Use .toThrowErrorMatchingSnapshot to test that a function throws a error
729 | * matching the most recent snapshot when it is called.
730 | */
731 | toThrowErrorMatchingSnapshot(): void,
732 | toThrowErrorMatchingInlineSnapshot(snapshot?: string): void,
733 | }
734 |
735 | type JestObjectType = {
736 | /**
737 | * Disables automatic mocking in the module loader.
738 | *
739 | * After this method is called, all `require()`s will return the real
740 | * versions of each module (rather than a mocked version).
741 | */
742 | disableAutomock(): JestObjectType,
743 | /**
744 | * An un-hoisted version of disableAutomock
745 | */
746 | autoMockOff(): JestObjectType,
747 | /**
748 | * Enables automatic mocking in the module loader.
749 | */
750 | enableAutomock(): JestObjectType,
751 | /**
752 | * An un-hoisted version of enableAutomock
753 | */
754 | autoMockOn(): JestObjectType,
755 | /**
756 | * Clears the mock.calls and mock.instances properties of all mocks.
757 | * Equivalent to calling .mockClear() on every mocked function.
758 | */
759 | clearAllMocks(): JestObjectType,
760 | /**
761 | * Resets the state of all mocks. Equivalent to calling .mockReset() on every
762 | * mocked function.
763 | */
764 | resetAllMocks(): JestObjectType,
765 | /**
766 | * Restores all mocks back to their original value.
767 | */
768 | restoreAllMocks(): JestObjectType,
769 | /**
770 | * Removes any pending timers from the timer system.
771 | */
772 | clearAllTimers(): void,
773 | /**
774 | * The same as `mock` but not moved to the top of the expectation by
775 | * babel-jest.
776 | */
777 | doMock(moduleName: string, moduleFactory?: any): JestObjectType,
778 | /**
779 | * The same as `unmock` but not moved to the top of the expectation by
780 | * babel-jest.
781 | */
782 | dontMock(moduleName: string): JestObjectType,
783 | /**
784 | * Returns a new, unused mock function. Optionally takes a mock
785 | * implementation.
786 | */
787 | fn, TReturn>(
788 | implementation?: (...args: TArguments) => TReturn
789 | ): JestMockFn,
790 | /**
791 | * Determines if the given function is a mocked function.
792 | */
793 | isMockFunction(fn: Function): boolean,
794 | /**
795 | * Given the name of a module, use the automatic mocking system to generate a
796 | * mocked version of the module for you.
797 | */
798 | genMockFromModule(moduleName: string): any,
799 | /**
800 | * Mocks a module with an auto-mocked version when it is being required.
801 | *
802 | * The second argument can be used to specify an explicit module factory that
803 | * is being run instead of using Jest's automocking feature.
804 | *
805 | * The third argument can be used to create virtual mocks -- mocks of modules
806 | * that don't exist anywhere in the system.
807 | */
808 | mock(
809 | moduleName: string,
810 | moduleFactory?: any,
811 | options?: Object
812 | ): JestObjectType,
813 | /**
814 | * Returns the actual module instead of a mock, bypassing all checks on
815 | * whether the module should receive a mock implementation or not.
816 | */
817 | requireActual(moduleName: string): any,
818 | /**
819 | * Returns a mock module instead of the actual module, bypassing all checks
820 | * on whether the module should be required normally or not.
821 | */
822 | requireMock(moduleName: string): any,
823 | /**
824 | * Resets the module registry - the cache of all required modules. This is
825 | * useful to isolate modules where local state might conflict between tests.
826 | */
827 | resetModules(): JestObjectType,
828 | /**
829 | * Exhausts the micro-task queue (usually interfaced in node via
830 | * process.nextTick).
831 | */
832 | runAllTicks(): void,
833 | /**
834 | * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(),
835 | * setInterval(), and setImmediate()).
836 | */
837 | runAllTimers(): void,
838 | /**
839 | * Exhausts all tasks queued by setImmediate().
840 | */
841 | runAllImmediates(): void,
842 | /**
843 | * Executes only the macro task queue (i.e. all tasks queued by setTimeout()
844 | * or setInterval() and setImmediate()).
845 | */
846 | advanceTimersByTime(msToRun: number): void,
847 | /**
848 | * Executes only the macro task queue (i.e. all tasks queued by setTimeout()
849 | * or setInterval() and setImmediate()).
850 | *
851 | * Renamed to `advanceTimersByTime`.
852 | */
853 | runTimersToTime(msToRun: number): void,
854 | /**
855 | * Executes only the macro-tasks that are currently pending (i.e., only the
856 | * tasks that have been queued by setTimeout() or setInterval() up to this
857 | * point)
858 | */
859 | runOnlyPendingTimers(): void,
860 | /**
861 | * Explicitly supplies the mock object that the module system should return
862 | * for the specified module. Note: It is recommended to use jest.mock()
863 | * instead.
864 | */
865 | setMock(moduleName: string, moduleExports: any): JestObjectType,
866 | /**
867 | * Indicates that the module system should never return a mocked version of
868 | * the specified module from require() (e.g. that it should always return the
869 | * real module).
870 | */
871 | unmock(moduleName: string): JestObjectType,
872 | /**
873 | * Instructs Jest to use fake versions of the standard timer functions
874 | * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick,
875 | * setImmediate and clearImmediate).
876 | */
877 | useFakeTimers(): JestObjectType,
878 | /**
879 | * Instructs Jest to use the real versions of the standard timer functions.
880 | */
881 | useRealTimers(): JestObjectType,
882 | /**
883 | * Creates a mock function similar to jest.fn but also tracks calls to
884 | * object[methodName].
885 | */
886 | spyOn(object: Object, methodName: string, accessType?: "get" | "set"): JestMockFn,
887 | /**
888 | * Set the default timeout interval for tests and before/after hooks in milliseconds.
889 | * Note: The default timeout interval is 5 seconds if this method is not called.
890 | */
891 | setTimeout(timeout: number): JestObjectType
892 | };
893 |
894 | type JestSpyType = {
895 | calls: JestCallsType
896 | };
897 |
898 | /** Runs this function after every test inside this context */
899 | declare function afterEach(
900 | fn: (done: () => void) => ?Promise,
901 | timeout?: number
902 | ): void;
903 | /** Runs this function before every test inside this context */
904 | declare function beforeEach(
905 | fn: (done: () => void) => ?Promise,
906 | timeout?: number
907 | ): void;
908 | /** Runs this function after all tests have finished inside this context */
909 | declare function afterAll(
910 | fn: (done: () => void) => ?Promise,
911 | timeout?: number
912 | ): void;
913 | /** Runs this function before any tests have started inside this context */
914 | declare function beforeAll(
915 | fn: (done: () => void) => ?Promise,
916 | timeout?: number
917 | ): void;
918 |
919 | /** A context for grouping tests together */
920 | declare var describe: {
921 | /**
922 | * Creates a block that groups together several related tests in one "test suite"
923 | */
924 | (name: JestTestName, fn: () => void): void,
925 |
926 | /**
927 | * Only run this describe block
928 | */
929 | only(name: JestTestName, fn: () => void): void,
930 |
931 | /**
932 | * Skip running this describe block
933 | */
934 | skip(name: JestTestName, fn: () => void): void,
935 |
936 | /**
937 | * each runs this test against array of argument arrays per each run
938 | *
939 | * @param {table} table of Test
940 | */
941 | each(
942 | ...table: Array | mixed> | [Array, string]
943 | ): (
944 | name: JestTestName,
945 | fn?: (...args: Array) => ?Promise,
946 | timeout?: number
947 | ) => void,
948 | };
949 |
950 | /** An individual test unit */
951 | declare var it: {
952 | /**
953 | * An individual test unit
954 | *
955 | * @param {JestTestName} Name of Test
956 | * @param {Function} Test
957 | * @param {number} Timeout for the test, in milliseconds.
958 | */
959 | (
960 | name: JestTestName,
961 | fn?: (done: () => void) => ?Promise,
962 | timeout?: number
963 | ): void,
964 |
965 | /**
966 | * Only run this test
967 | *
968 | * @param {JestTestName} Name of Test
969 | * @param {Function} Test
970 | * @param {number} Timeout for the test, in milliseconds.
971 | */
972 | only(
973 | name: JestTestName,
974 | fn?: (done: () => void) => ?Promise,
975 | timeout?: number
976 | ): {
977 | each(
978 | ...table: Array | mixed> | [Array, string]
979 | ): (
980 | name: JestTestName,
981 | fn?: (...args: Array) => ?Promise,
982 | timeout?: number
983 | ) => void,
984 | },
985 |
986 | /**
987 | * Skip running this test
988 | *
989 | * @param {JestTestName} Name of Test
990 | * @param {Function} Test
991 | * @param {number} Timeout for the test, in milliseconds.
992 | */
993 | skip(
994 | name: JestTestName,
995 | fn?: (done: () => void) => ?Promise,
996 | timeout?: number
997 | ): void,
998 |
999 | /**
1000 | * Run the test concurrently
1001 | *
1002 | * @param {JestTestName} Name of Test
1003 | * @param {Function} Test
1004 | * @param {number} Timeout for the test, in milliseconds.
1005 | */
1006 | concurrent(
1007 | name: JestTestName,
1008 | fn?: (done: () => void) => ?Promise,
1009 | timeout?: number
1010 | ): void,
1011 |
1012 | /**
1013 | * each runs this test against array of argument arrays per each run
1014 | *
1015 | * @param {table} table of Test
1016 | */
1017 | each(
1018 | ...table: Array | mixed> | [Array, string]
1019 | ): (
1020 | name: JestTestName,
1021 | fn?: (...args: Array) => ?Promise,
1022 | timeout?: number
1023 | ) => void,
1024 | };
1025 |
1026 | declare function fit(
1027 | name: JestTestName,
1028 | fn: (done: () => void) => ?Promise,
1029 | timeout?: number
1030 | ): void;
1031 | /** An individual test unit */
1032 | declare var test: typeof it;
1033 | /** A disabled group of tests */
1034 | declare var xdescribe: typeof describe;
1035 | /** A focused group of tests */
1036 | declare var fdescribe: typeof describe;
1037 | /** A disabled individual test */
1038 | declare var xit: typeof it;
1039 | /** A disabled individual test */
1040 | declare var xtest: typeof it;
1041 |
1042 | type JestPrettyFormatColors = {
1043 | comment: { close: string, open: string },
1044 | content: { close: string, open: string },
1045 | prop: { close: string, open: string },
1046 | tag: { close: string, open: string },
1047 | value: { close: string, open: string },
1048 | };
1049 |
1050 | type JestPrettyFormatIndent = string => string;
1051 | type JestPrettyFormatRefs = Array;
1052 | type JestPrettyFormatPrint = any => string;
1053 | type JestPrettyFormatStringOrNull = string | null;
1054 |
1055 | type JestPrettyFormatOptions = {|
1056 | callToJSON: boolean,
1057 | edgeSpacing: string,
1058 | escapeRegex: boolean,
1059 | highlight: boolean,
1060 | indent: number,
1061 | maxDepth: number,
1062 | min: boolean,
1063 | plugins: JestPrettyFormatPlugins,
1064 | printFunctionName: boolean,
1065 | spacing: string,
1066 | theme: {|
1067 | comment: string,
1068 | content: string,
1069 | prop: string,
1070 | tag: string,
1071 | value: string,
1072 | |},
1073 | |};
1074 |
1075 | type JestPrettyFormatPlugin = {
1076 | print: (
1077 | val: any,
1078 | serialize: JestPrettyFormatPrint,
1079 | indent: JestPrettyFormatIndent,
1080 | opts: JestPrettyFormatOptions,
1081 | colors: JestPrettyFormatColors,
1082 | ) => string,
1083 | test: any => boolean,
1084 | };
1085 |
1086 | type JestPrettyFormatPlugins = Array;
1087 |
1088 | /** The expect function is used every time you want to test a value */
1089 | declare var expect: {
1090 | /** The object that you want to make assertions against */
1091 | (value: any):
1092 | & JestExpectType
1093 | & JestPromiseType
1094 | & EnzymeMatchersType
1095 | & DomTestingLibraryType
1096 | & JestJQueryMatchersType
1097 | & JestStyledComponentsMatchersType
1098 | & JestExtendedMatchersType,
1099 |
1100 | /** Add additional Jasmine matchers to Jest's roster */
1101 | extend(matchers: { [name: string]: JestMatcher }): void,
1102 | /** Add a module that formats application-specific data structures. */
1103 | addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void,
1104 | assertions(expectedAssertions: number): void,
1105 | hasAssertions(): void,
1106 | any(value: mixed): JestAsymmetricEqualityType,
1107 | anything(): any,
1108 | arrayContaining(value: Array): Array,
1109 | objectContaining(value: Object): Object,
1110 | /** Matches any received string that contains the exact expected string. */
1111 | stringContaining(value: string): string,
1112 | stringMatching(value: string | RegExp): string,
1113 | not: {
1114 | arrayContaining: (value: $ReadOnlyArray) => Array,
1115 | objectContaining: (value: {}) => Object,
1116 | stringContaining: (value: string) => string,
1117 | stringMatching: (value: string | RegExp) => string,
1118 | },
1119 | };
1120 |
1121 | // TODO handle return type
1122 | // http://jasmine.github.io/2.4/introduction.html#section-Spies
1123 | declare function spyOn(value: mixed, method: string): Object;
1124 |
1125 | /** Holds all functions related to manipulating test runner */
1126 | declare var jest: JestObjectType;
1127 |
1128 | /**
1129 | * The global Jasmine object, this is generally not exposed as the public API,
1130 | * using features inside here could break in later versions of Jest.
1131 | */
1132 | declare var jasmine: {
1133 | DEFAULT_TIMEOUT_INTERVAL: number,
1134 | any(value: mixed): JestAsymmetricEqualityType,
1135 | anything(): any,
1136 | arrayContaining(value: Array): Array,
1137 | clock(): JestClockType,
1138 | createSpy(name: string): JestSpyType,
1139 | createSpyObj(
1140 | baseName: string,
1141 | methodNames: Array
1142 | ): { [methodName: string]: JestSpyType },
1143 | objectContaining(value: Object): Object,
1144 | stringMatching(value: string): string
1145 | };
1146 |
--------------------------------------------------------------------------------