Skip to content
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
Expand Up @@ -7,6 +7,7 @@
export { mockHistory } from './react_router_history.mock';
export { mockKibanaContext } from './kibana_context.mock';
export { mockLicenseContext } from './license_context.mock';
export { mountWithKibanaContext } from './mount_with_context.mock';
export { mountWithContext, mountWithKibanaContext } from './mount_with_context.mock';
export { shallowWithIntl } from './shallow_with_i18n.mock';

// Note: shallow_usecontext must be imported directly as a file
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,43 @@
import React from 'react';
import { mount } from 'enzyme';

import { I18nProvider } from '@kbn/i18n/react';
import { KibanaContext } from '../';
import { mockKibanaContext } from './kibana_context.mock';
import { LicenseContext } from '../shared/licensing';
import { mockLicenseContext } from './license_context.mock';

/**
* This helper mounts a component with a set of default KibanaContext,
* while also allowing custom context to be passed in via a second arg
* This helper mounts a component with all the contexts/providers used
* by the production app, while allowing custom context to be
* passed in via a second arg
*
* Example usage:
*
* const wrapper = mountWithKibanaContext(<Component />, { enterpriseSearchUrl: 'someOverride' });
* const wrapper = mountWithContext(<Component />, { enterpriseSearchUrl: 'someOverride', license: {} });
*/
export const mountWithKibanaContext = (node, contextProps) => {
export const mountWithContext = (children, context) => {
return mount(
<KibanaContext.Provider value={{ ...mockKibanaContext, ...contextProps }}>
{node}
<I18nProvider>
<KibanaContext.Provider value={{ ...mockKibanaContext, ...context }}>
<LicenseContext.Provider value={{ ...mockLicenseContext, ...context }}>
{children}
</LicenseContext.Provider>
</KibanaContext.Provider>
</I18nProvider>
);
};

/**
* This helper mounts a component with just the default KibanaContext -
* useful for isolated / helper components that only need this context
*
* Same usage/override functionality as mountWithContext
*/
export const mountWithKibanaContext = (children, context) => {
return mount(
<KibanaContext.Provider value={{ ...mockKibanaContext, ...context }}>
{children}
</KibanaContext.Provider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 { shallow } from 'enzyme';
import { I18nProvider } from '@kbn/i18n/react';
import { IntlProvider } from 'react-intl';

const intlProvider = new IntlProvider({ locale: 'en', messages: {} }, {});
const { intl } = intlProvider.getChildContext();
Comment on lines +12 to +13
Copy link
Owner Author

Choose a reason for hiding this comment

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


/**
* This helper shallow wraps a component with react-intl's <I18nProvider> which
* fixes "Could not find required `intl` object" console errors when running tests
*
* Example usage (should be the same as shallow()):
*
* const wrapper = shallowWithIntl(<Component />);
*/
export const shallowWithIntl = children => {
return shallow(<I18nProvider>{children}</I18nProvider>, {
context: { intl },
childContextTypes: { intl },
})
Comment on lines +24 to +27
Copy link
Owner Author

Choose a reason for hiding this comment

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

Without this, you get a bunch of these console "errors" when running tests (even though the tests themselves pass):

[React Intl] Could not find required `intl` object. <IntlProvider> needs to exist in the component ancestry. Using default message as fallback.

.childAt(0)
.dive()
.shallow();
Comment on lines +28 to +30
Copy link
Owner Author

Choose a reason for hiding this comment

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

This returns the underlying <Child /> component instead of (<I18nProvider><Child /></I18nProvider>, which makes .find()ing children less of a pain.

I had to do this a bunch on older projects for HOC wrappers, I can 10000% say I don't miss it 😄

};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import React, { useContext } from 'react';
import { EuiPage, EuiPageBody, EuiPageContent, EuiEmptyPrompt, EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';

import { sendTelemetry } from '../../../shared/telemetry';
import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs';
Expand Down Expand Up @@ -39,17 +40,34 @@ export const EmptyState: React.FC<> = () => {
<EuiPageContent>
<EuiEmptyPrompt
iconType="eyeClosed"
title={<h2>There’s nothing here yet</h2>}
title={
<h2>
<FormattedMessage
id="xpack.appSearch.emptyState.title"
defaultMessage="There’s nothing here yet"
/>
</h2>
}
titleSize="l"
body={
<p>
Looks like you don’t have any App Search engines.
<br /> Let’s create your first one now.
<FormattedMessage
id="xpack.appSearch.emptyState.description1"
defaultMessage="Looks like you don’t have any App Search engines."
/>
<br />
<FormattedMessage
id="xpack.appSearch.emptyState.description2"
defaultMessage="Let’s create your first one now."
/>
</p>
}
actions={
<EuiButton iconType="popout" fill {...buttonProps}>
Create your first Engine
<FormattedMessage
id="xpack.appSearch.emptyState.createFirstEngineCta"
defaultMessage="Create your first Engine"
/>
</EuiButton>
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import '../../../__mocks__/shallow_usecontext.mock';
import React from 'react';
import { shallow } from 'enzyme';
import { EuiEmptyPrompt, EuiButton, EuiCode, EuiLoadingContent } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { shallowWithIntl } from '../../../__mocks__';

jest.mock('../../utils/get_username', () => ({ getUserName: jest.fn() }));
import { getUserName } from '../../utils/get_username';
Expand All @@ -24,38 +26,36 @@ 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>);
expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
});
});

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>);
expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
});

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

expect(prompt.find(EuiCode).prop('children')).toContain('dolores-abernathy');
expect(description1.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>);
expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
});

it('sends telemetry on create first engine click', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import React, { useContext } from 'react';
import { EuiPage, EuiPageBody, EuiPageContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';

import { EuiButton } from '../../../shared/react_router_helpers';
import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs';
Expand All @@ -29,23 +30,43 @@ export const ErrorState: ReactFC<> = () => {
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
title={<h2>Cannot connect to App Search</h2>}
title={
<h2>
<FormattedMessage
id="xpack.appSearch.errorConnectingState.title"
defaultMessage="Cannot connect to App Search"
/>
</h2>
}
titleSize="l"
body={
<>
<p>
We cannot connect to the App Search instance at the configured host URL:{' '}
<EuiCode>{enterpriseSearchUrl}</EuiCode>
<FormattedMessage
id="xpack.appSearch.errorConnectingState.description1"
defaultMessage="We cannot connect to the App Search instance at the configured host URL: {enterpriseSearchUrl}"
values={{
enterpriseSearchUrl: <EuiCode>{enterpriseSearchUrl}</EuiCode>,
}}
/>
</p>
<p>
Please ensure your App Search host URL is configured correctly within{' '}
<EuiCode>config/kibana.yml</EuiCode>.
<FormattedMessage
id="xpack.appSearch.errorConnectingState.description2"
defaultMessage="Please ensure your App Search host URL is configured correctly within {configFile}."
values={{
configFile: <EuiCode>config/kibana.yml</EuiCode>,
}}
/>
</p>
</>
}
actions={
<EuiButton iconType="help" fill to="/setup_guide">
Review the setup guide
<FormattedMessage
id="xpack.appSearch.errorConnectingState.setupGuideCta"
defaultMessage="Review the setup guide"
/>
</EuiButton>
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import React from 'react';
import { EuiPage, EuiPageBody, EuiPageContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';

import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs';
import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
Expand All @@ -27,21 +28,31 @@ export const NoUserState: React.FC<> = () => {
<EuiPageContent>
<EuiEmptyPrompt
iconType="lock"
title={<h2>Cannot find App Search account</h2>}
title={
<h2>
<FormattedMessage
id="xpack.appSearch.noUserState.title"
defaultMessage="Cannot find App Search account"
/>
</h2>
}
titleSize="l"
body={
<>
<p>
We cannot find an App Search account matching your username
{username && (
<>
: <EuiCode>{username}</EuiCode>
</>
)}
.
<FormattedMessage
id="xpack.appSearch.noUserState.description1"
defaultMessage="We cannot find an App Search account matching your username{username}."
values={{
username: username ? <EuiCode>{username}</EuiCode> : '',
}}
/>
</p>
<p>
Please contact your App Search administrator to request an invite for that user.
<FormattedMessage
id="xpack.appSearch.noUserState.description2"
defaultMessage="Please contact your App Search administrator to request an invite for that user."
/>
</p>
</>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { render } from 'enzyme';

import { I18nProvider } from '@kbn/i18n/react';
import { KibanaContext } from '../../../';
import { LicenseContext } from '../../../shared/licensing';
import { mountWithKibanaContext, mockKibanaContext } from '../../../__mocks__';
import { mountWithContext, mockKibanaContext } from '../../../__mocks__';

import { EmptyState, ErrorState, NoUserState } from '../empty_states';
import { EngineTable } from './engine_table';
Expand All @@ -23,12 +24,15 @@ describe('EngineOverview', () => {
describe('non-happy-path states', () => {
it('isLoading', () => {
// We use render() instead of mount() here to not trigger lifecycle methods (i.e., useEffect)
// TODO: Consider pulling this out to a renderWithContext mock/helper
const wrapper = render(
<KibanaContext.Provider value={{ http: {} }}>
<LicenseContext.Provider value={{ license: {} }}>
<EngineOverview />
</LicenseContext.Provider>
</KibanaContext.Provider>
<I18nProvider>
<KibanaContext.Provider value={{ http: {} }}>
<LicenseContext.Provider value={{ license: {} }}>
<EngineOverview />
</LicenseContext.Provider>
</KibanaContext.Provider>
</I18nProvider>
);

// render() directly renders HTML which means we have to look for selectors instead of for LoadingState directly
Expand Down Expand Up @@ -66,7 +70,7 @@ describe('EngineOverview', () => {
results: [
{
name: 'hello-world',
created_at: 'somedate',
created_at: 'Fri, 1 Jan 1970 12:00:00 +0000',
document_count: 50,
field_count: 10,
},
Expand Down Expand Up @@ -164,12 +168,7 @@ describe('EngineOverview', () => {
// 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(
<LicenseContext.Provider value={{ license }}>
<EngineOverview />
</LicenseContext.Provider>,
{ http: httpMock }
);
wrapper = mountWithContext(<EngineOverview />, { http: httpMock, license });
});
wrapper.update(); // This seems to be required for the DOM to actually update

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
EuiTitle,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';

import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs';
import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
Expand Down Expand Up @@ -100,7 +101,10 @@ export const EngineOverview: ReactFC<> = () => {
<EuiTitle size="s">
<h2>
<img src={EnginesIcon} alt="" className="engine-icon" />
Engines
<FormattedMessage
id="xpack.appSearch.enginesOverview.engines"
defaultMessage="Engines"
/>
</h2>
</EuiTitle>
</EuiPageContentHeader>
Expand All @@ -122,7 +126,10 @@ export const EngineOverview: ReactFC<> = () => {
<EuiTitle size="s">
<h2>
<img src={MetaEnginesIcon} alt="" className="engine-icon" />
Meta Engines
<FormattedMessage
id="xpack.appSearch.enginesOverview.metaEngines"
defaultMessage="Meta Engines"
/>
</h2>
</EuiTitle>
</EuiPageContentHeader>
Expand Down
Loading