Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Implement remaining unit tests #7

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 '../../../test_utils/mock_shallow_usecontext';

import React from 'react';
import { shallow } from 'enzyme';
import { EuiEmptyPrompt, EuiCode, EuiLoadingContent } from '@elastic/eui';

jest.mock('../../utils/get_username', () => ({ getUserName: jest.fn() }));
import { getUserName } from '../../utils/get_username';

import { ErrorState, NoUserState, EmptyState, LoadingState } from './';

describe('ErrorState', () => {
it('renders', () => {
const wrapper = shallow(<ErrorState />);
const prompt = wrapper.find(EuiEmptyPrompt);

expect(prompt).toHaveLength(1);
expect(prompt.prop('title')).toEqual(<h2>Cannot connect to App Search</h2>);
});
});

describe('NoUserState', () => {
it('renders', () => {
const wrapper = shallow(<NoUserState />);
const prompt = wrapper.find(EuiEmptyPrompt);

expect(prompt).toHaveLength(1);
expect(prompt.prop('title')).toEqual(<h2>Cannot find App Search account</h2>);
});

it('renders with username', () => {
getUserName.mockImplementationOnce(() => 'dolores-abernathy');
const wrapper = shallow(<NoUserState />);
const prompt = wrapper.find(EuiEmptyPrompt).dive();

expect(prompt.find(EuiCode).prop('children')).toContain('dolores-abernathy');
});
});

describe('EmptyState', () => {
it('renders', () => {
const wrapper = shallow(<EmptyState />);
const prompt = wrapper.find(EuiEmptyPrompt);

expect(prompt).toHaveLength(1);
expect(prompt.prop('title')).toEqual(<h2>There’s nothing here yet</h2>);
});
});

describe('LoadingState', () => {
it('renders', () => {
const wrapper = shallow(<LoadingState />);

expect(wrapper.find(EuiLoadingContent)).toHaveLength(2);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* 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 '../../../test_utils/mock_rr_usehistory';

import React from 'react';
import { act } from 'react-dom/test-utils';
import { render } from 'enzyme';

import { KibanaContext } from '../../../';
import { mountWithKibanaContext, mockKibanaContext } from '../../../test_utils';

import { EmptyState, ErrorState, NoUserState } from '../empty_states';
import { EngineTable } from './engine_table';

import { EngineOverview } from './';

describe('EngineOverview', () => {
describe('non-happy-path states', () => {
it('isLoading', () => {
// We use render() instead of mount() here to not trigger lifecycle methods (i.e., useEffect)
const wrapper = render(
<KibanaContext.Provider value={{ http: {} }}>
<EngineOverview />
</KibanaContext.Provider>
);

// render() directly renders HTML which means we have to look for selectors instead of for LoadingState directly
expect(wrapper.find('.euiLoadingContent')).toHaveLength(2);
});

it('isEmpty', async () => {
const wrapper = await mountWithApiMock({
get: () => ({
results: [],
meta: { page: { total_results: 0 } },
}),
});

expect(wrapper.find(EmptyState)).toHaveLength(1);
});

it('hasErrorConnecting', async () => {
const wrapper = await mountWithApiMock({
get: () => ({ invalidPayload: true }),
});
expect(wrapper.find(ErrorState)).toHaveLength(1);
});

it('hasNoAccount', async () => {
const wrapper = await mountWithApiMock({
get: () => ({ message: 'no-as-account' }),
});
expect(wrapper.find(NoUserState)).toHaveLength(1);
});
});

describe('happy-path states', () => {
const mockedApiResponse = {
results: [
{
name: 'hello-world',
created_at: 'somedate',
document_count: 50,
field_count: 10,
},
],
meta: {
page: {
current: 1,
total_pages: 10,
total_results: 100,
size: 10,
},
},
};
const mockApi = jest.fn(() => mockedApiResponse);
let wrapper;

beforeAll(async () => {
wrapper = await mountWithApiMock({ get: mockApi });
});

it('renders', () => {
expect(wrapper.find(EngineTable)).toHaveLength(2);
});

it('calls the engines API', () => {
expect(mockApi).toHaveBeenNthCalledWith(1, '/api/app_search/engines', {
query: {
type: 'indexed',
pageIndex: 1,
},
});
expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', {
query: {
type: 'meta',
pageIndex: 1,
},
});
});

describe('pagination', () => {
const getTablePagination = () =>
wrapper
.find(EngineTable)
.first()
.prop('pagination');

it('passes down page data from the API', () => {
const pagination = getTablePagination();

expect(pagination.totalEngines).toEqual(100);
expect(pagination.pageIndex).toEqual(0);
});

it('re-polls the API on page change', async () => {
await act(async () => getTablePagination().onPaginate(5));
wrapper.update();

expect(mockApi).toHaveBeenLastCalledWith('/api/app_search/engines', {
query: {
type: 'indexed',
pageIndex: 5,
},
});
expect(getTablePagination().pageIndex).toEqual(4);
});
});
});

/**
* Test helpers
*/

const mountWithApiMock = async ({ get }) => {
let wrapper;
const httpMock = { ...mockKibanaContext.http, get };

// We get a lot of act() warning/errors in the terminal without this.
// TBH, I don't fully understand why since Enzyme's mount is supposed to
// have act() baked in - could be because of the wrapping context provider?
await act(async () => {
wrapper = mountWithKibanaContext(<EngineOverview />, { http: httpMock });
});
wrapper.update(); // This seems to be required for the DOM to actually update

return wrapper;
};
});
Original file line number Diff line number Diff line change
Expand Up @@ -42,35 +42,40 @@ export const EngineOverview: ReactFC<> = () => {
const [metaEnginesPage, setMetaEnginesPage] = useState(1);
const [metaEnginesTotal, setMetaEnginesTotal] = useState(0);

const getEnginesData = ({ type, pageIndex }) => {
return http.get('/api/app_search/engines', {
const getEnginesData = async ({ type, pageIndex }) => {
return await http.get('/api/app_search/engines', {
query: { type, pageIndex },
});
};
const hasValidData = response => {
return response && response.results && response.meta;
return (
response &&
Array.isArray(response.results) &&
response.meta &&
response.meta.page &&
typeof response.meta.page.total_results === 'number'
); // TODO: Move to optional chaining once Prettier has been updated to support it
};
const hasNoAccountError = response => {
return response && response.message === 'no-as-account';
};
const setEnginesData = (params, callbacks) => {
getEnginesData(params)
.then(response => {
if (!hasValidData(response)) {
if (hasNoAccountError(response)) {
return setHasNoAccount(true);
}
throw new Error('App Search engines response is missing valid data');
const setEnginesData = async (params, callbacks) => {
try {
const response = await getEnginesData(params);
if (!hasValidData(response)) {
if (hasNoAccountError(response)) {
return setHasNoAccount(true);
}

callbacks.setResults(response.results);
callbacks.setResultsTotal(response.meta.page.total_results);
setIsLoading(false);
})
.catch(error => {
// TODO - should we be logging errors to telemetry or elsewhere for debugging?
setHasErrorConnecting(true);
});
throw new Error('App Search engines response is missing valid data');
}

callbacks.setResults(response.results);
callbacks.setResultsTotal(response.meta.page.total_results);
setIsLoading(false);
} catch (error) {
// TODO - should we be logging errors to telemetry or elsewhere for debugging?
setHasErrorConnecting(true);
}
};

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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 { EuiBasicTable, EuiPagination, EuiButtonEmpty, EuiLink } from '@elastic/eui';

import { mountWithKibanaContext } from '../../../test_utils';
jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() }));
import { sendTelemetry } from '../../../shared/telemetry';

import { EngineTable } from './engine_table';

describe('EngineTable', () => {
const onPaginate = jest.fn(); // onPaginate updates the engines API call upstream

const wrapper = mountWithKibanaContext(
<EngineTable
data={[
{
name: 'test-engine',
created_at: 'Fri, 1 Jan 1970 12:00:00 +0000',
document_count: 99999,
field_count: 10,
},
]}
pagination={{
totalEngines: 50,
pageIndex: 0,
onPaginate,
}}
/>
);
const table = wrapper.find(EuiBasicTable);

it('renders', () => {
expect(table).toHaveLength(1);
expect(table.prop('pagination').totalItemCount).toEqual(50);

const tableContent = table.text();
expect(tableContent).toContain('test-engine');
expect(tableContent).toContain('January 1, 1970');
expect(tableContent).toContain('99,999');
expect(tableContent).toContain('10');

expect(table.find(EuiPagination).find(EuiButtonEmpty)).toHaveLength(5); // Should display 5 pages at 10 engines per page
});

it('contains engine links which send telemetry', () => {
const engineLinks = wrapper.find(EuiLink);

engineLinks.forEach(link => {
expect(link.prop('href')).toEqual('http://localhost:3002/as/engines/test-engine');
link.simulate('click');

expect(sendTelemetry).toHaveBeenCalledWith({
http: expect.any(Object),
product: 'app_search',
action: 'clicked',
metric: 'engine_table_link',
});
});
});

it('triggers onPaginate', () => {
table.prop('onChange')({ page: { index: 4 } });

expect(onPaginate).toHaveBeenCalledWith(5);
});

it('handles empty data', () => {
const emptyWrapper = mountWithKibanaContext(
<EngineTable data={[]} pagination={{ totalEngines: 0 }} />
);
const emptyTable = wrapper.find(EuiBasicTable);
expect(emptyTable.prop('pagination').pageIndex).toEqual(0);
});
});
Loading