Skip to content

Commit

Permalink
Add test for hooks and minor cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
sorenlouv committed Mar 28, 2019
1 parent 106b0d7 commit 6deda74
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 61 deletions.
1 change: 1 addition & 0 deletions x-pack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@
"react-dom": "^16.8.0",
"react-dropzone": "^4.2.9",
"react-fast-compare": "^2.0.4",
"react-hooks-testing-library": "^0.3.8",
"react-markdown-renderer": "^1.4.0",
"react-portal": "^3.2.0",
"react-redux": "^5.0.7",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

import { mount } from 'enzyme';
import { Location } from 'history';
import createHistory from 'history/createHashHistory';
import PropTypes from 'prop-types';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import {
mockMoment,
mountWithRouterAndStore,
toJson
} from '../../../../../utils/testHelpers';
// @ts-ignore
import { createMockStore } from 'redux-test-utils';
import { mockMoment, toJson } from '../../../../../utils/testHelpers';
import { ErrorGroupList } from '../index';
import props from './props.json';

Expand Down Expand Up @@ -47,3 +47,30 @@ describe('ErrorGroupOverview -> List', () => {
expect(toJson(wrapper)).toMatchSnapshot();
});
});

export function mountWithRouterAndStore(
Component: React.ReactElement,
storeState = {}
) {
const store = createMockStore(storeState);
const history = createHistory();

const options = {
context: {
store,
router: {
history,
route: {
match: { path: '/', url: '/', params: {}, isExact: true },
location: { pathname: '/', search: '', hash: '', key: '4yyjf5' }
}
}
},
childContextTypes: {
store: PropTypes.object.isRequired,
router: PropTypes.object.isRequired
}
};

return mount(Component, options);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { MemoryRouter } from 'react-router-dom';
import { Store } from 'redux';
// @ts-ignore
import configureStore from 'x-pack/plugins/apm/public/store/config/configureStore';
import { mockNow } from 'x-pack/plugins/apm/public/utils/testHelpers';
import { mockNow, tick } from 'x-pack/plugins/apm/public/utils/testHelpers';
import { DatePicker, DatePickerComponent } from '../DatePicker';

function mountPicker(initialState = {}) {
Expand Down Expand Up @@ -54,8 +54,6 @@ describe('DatePicker', () => {
});
});

const tick = () => new Promise(resolve => setImmediate(resolve, 0));

describe('refresh cycle', () => {
let nowSpy: jest.Mock;
beforeEach(() => {
Expand Down
121 changes: 121 additions & 0 deletions x-pack/plugins/apm/public/hooks/useFetcher.integration.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import { render } from 'react-testing-library';
import { delay, tick } from '../utils/testHelpers';
import { useFetcher } from './useFetcher';

// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769
/* tslint:disable:no-console */
const originalError = console.error;
beforeAll(() => {
console.error = jest.fn();
});
afterAll(() => {
console.error = originalError;
});

async function asyncFn(name: string, ms: number) {
await delay(ms);
return `Hello from ${name}`;
}

describe('when simulating race condition', () => {
let requestCallOrder: Array<[string, string, number]>;
let renderSpy: jest.Mock;

beforeEach(async () => {
jest.useFakeTimers();
jest
.spyOn(window, 'requestAnimationFrame')
.mockImplementation(cb => cb(0) as any);

renderSpy = jest.fn();
requestCallOrder = [];

function MyComponent({
name,
ms,
renderFn
}: {
name: string;
ms: number;
renderFn: any;
}) {
const { data, status, error } = useFetcher(
async () => {
requestCallOrder.push(['request', name, ms]);
const res = await asyncFn(name, ms);
requestCallOrder.push(['response', name, ms]);
return res;
},
[name, ms]
);
renderFn({ data, status, error });
return null;
}

const { rerender } = render(
<MyComponent name="John" ms={500} renderFn={renderSpy} />
);

rerender(<MyComponent name="Peter" ms={100} renderFn={renderSpy} />);
});

it('should render initially render loading state', async () => {
expect(renderSpy).lastCalledWith({
data: undefined,
error: undefined,
status: 'loading'
});
});

it('should render "Hello from Peter" after 200ms', async () => {
jest.advanceTimersByTime(200);
await tick();

expect(renderSpy).lastCalledWith({
data: 'Hello from Peter',
error: undefined,
status: 'success'
});
});

it('should render "Hello from Peter" after 600ms', async () => {
jest.advanceTimersByTime(600);
await tick();

expect(renderSpy).lastCalledWith({
data: 'Hello from Peter',
error: undefined,
status: 'success'
});
});

it('should should NOT have rendered "Hello from John" at any point', async () => {
jest.advanceTimersByTime(600);
await tick();

expect(renderSpy).not.toHaveBeenCalledWith({
data: 'Hello from John',
error: undefined,
status: 'success'
});
});

it('should send and receive calls in the right order', async () => {
jest.advanceTimersByTime(600);
await tick();

expect(requestCallOrder).toEqual([
['request', 'John', 500],
['request', 'Peter', 100],
['response', 'Peter', 100],
['response', 'John', 500]
]);
});
});
61 changes: 61 additions & 0 deletions x-pack/plugins/apm/public/hooks/useFetcher.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { cleanup, renderHook } from 'react-hooks-testing-library';
import { delay } from '../utils/testHelpers';
import { useFetcher } from './useFetcher';

afterEach(cleanup);

// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769
/* tslint:disable:no-console */
const originalError = console.error;
beforeAll(() => {
console.error = jest.fn();
});
afterAll(() => {
console.error = originalError;
});

describe('useFetcher', () => {
let output: ReturnType<typeof renderHook>;
beforeEach(() => {
jest.useFakeTimers();
async function fn() {
await delay(500);
return 'response from hook';
}
output = renderHook(() => useFetcher(() => fn(), []));
});

it('should initially be empty', async () => {
expect(output.result.current).toEqual({
data: undefined,
error: undefined,
status: undefined
});
});

it('should show loading spinner', async () => {
await output.waitForNextUpdate();
expect(output.result.current).toEqual({
data: undefined,
error: undefined,
status: 'loading'
});
});

it('should show success after 1 second', async () => {
jest.advanceTimersByTime(1000);
await output.waitForNextUpdate();

expect(output.result.current).toEqual({
data: 'response from hook',
error: undefined,
status: 'success'
});
});
});
61 changes: 8 additions & 53 deletions x-pack/plugins/apm/public/utils/testHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@

import { mount, ReactWrapper } from 'enzyme';
import enzymeToJson from 'enzyme-to-json';
import createHistory from 'history/createHashHistory';
import 'jest-styled-components';
import moment from 'moment';
import { Moment } from 'moment-timezone';
import PropTypes from 'prop-types';
import React from 'react';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
Expand All @@ -29,51 +27,6 @@ export function toJson(wrapper: ReactWrapper) {
});
}

const defaultRoute = {
match: { path: '/', url: '/', params: {}, isExact: true },
location: { pathname: '/', search: '', hash: '', key: '4yyjf5' }
};

export function mountWithRouterAndStore(
Component: React.ReactElement,
storeState = {},
route = defaultRoute
) {
const store = createMockStore(storeState);
const history = createHistory();

const options = {
context: {
store,
router: {
history,
route
}
},
childContextTypes: {
store: PropTypes.object.isRequired,
router: PropTypes.object.isRequired
}
};

return mount(Component, options);
}

export function mountWithStore(Component: React.ReactElement, storeState = {}) {
const store = createMockStore(storeState);

const options = {
context: {
store
},
childContextTypes: {
store: PropTypes.object.isRequired
}
};

return mount(Component, options);
}

export function mockMoment() {
// avoid timezone issues
jest
Expand All @@ -90,11 +43,6 @@ export function mockMoment() {
});
}

// Await this when you need to "flush" promises to immediately resolve or throw in tests
export async function asyncFlush() {
return new Promise(resolve => setTimeout(resolve, 0));
}

// Useful for getting the rendered href from any kind of link component
export async function getRenderedHref(
Component: React.FunctionComponent<{}>,
Expand All @@ -109,7 +57,7 @@ export async function getRenderedHref(
</Provider>
);

await asyncFlush();
await tick();

return mounted.render().attr('href');
}
Expand All @@ -118,3 +66,10 @@ export function mockNow(date: string) {
const fakeNow = new Date(date).getTime();
return jest.spyOn(Date, 'now').mockReturnValue(fakeNow);
}

export function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

// Await this when you need to "flush" promises to immediately resolve or throw in tests
export const tick = () => new Promise(resolve => setImmediate(resolve, 0));
Loading

0 comments on commit 6deda74

Please sign in to comment.