mirror of
https://github.com/SomboChea/ui
synced 2024-11-24 06:54:27 +07:00
Search Component - Replaced class by func. comp (#339)
This commit is contained in:
parent
09b831a40d
commit
6ba721446b
@ -45,11 +45,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"@typescript-eslint/explicit-function-return-type": ["warn",
|
"@typescript-eslint/explicit-function-return-type": 0,
|
||||||
{
|
"react/display-name": 0,
|
||||||
"allowExpressions": true,
|
|
||||||
"allowTypedFunctionExpressions": true
|
|
||||||
}],
|
|
||||||
"react/no-deprecated": 1,
|
"react/no-deprecated": 1,
|
||||||
"react/jsx-no-target-blank": 1,
|
"react/jsx-no-target-blank": 1,
|
||||||
"react/destructuring-assignment": ["error", "always"],
|
"react/destructuring-assignment": ["error", "always"],
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/explicit-function-return-type": 0
|
|
||||||
}
|
|
||||||
}
|
|
@ -22,6 +22,7 @@
|
|||||||
"@material-ui/core": "4.7.1",
|
"@material-ui/core": "4.7.1",
|
||||||
"@material-ui/icons": "4.5.1",
|
"@material-ui/icons": "4.5.1",
|
||||||
"@octokit/rest": "16.35.0",
|
"@octokit/rest": "16.35.0",
|
||||||
|
"@testing-library/jest-dom": "4.2.4",
|
||||||
"@testing-library/react": "9.3.2",
|
"@testing-library/react": "9.3.2",
|
||||||
"@types/autosuggest-highlight": "3.1.0",
|
"@types/autosuggest-highlight": "3.1.0",
|
||||||
"@types/enzyme": "3.10.3",
|
"@types/enzyme": "3.10.3",
|
||||||
|
@ -27,7 +27,6 @@ const StyledBoxContent = styled(Box)({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/* eslint-disable react/jsx-max-depth */
|
/* eslint-disable react/jsx-max-depth */
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
|
||||||
/* eslint-disable react/jsx-no-bind */
|
/* eslint-disable react/jsx-no-bind */
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
|
@ -9,7 +9,6 @@ interface Props {
|
|||||||
user?: User;
|
user?: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
const AppContextProvider: React.FC<Props> = ({ children, packages, user }) => {
|
const AppContextProvider: React.FC<Props> = ({ children, packages, user }) => {
|
||||||
const [state, setState] = useState<AppProps>({
|
const [state, setState] = useState<AppProps>({
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { KeyboardEvent } from 'react';
|
import React, { KeyboardEvent, memo } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import Autosuggest, { SuggestionSelectedEventData, InputProps, ChangeEvent } from 'react-autosuggest';
|
import Autosuggest, { SuggestionSelectedEventData, InputProps, ChangeEvent } from 'react-autosuggest';
|
||||||
import match from 'autosuggest-highlight/match';
|
import match from 'autosuggest-highlight/match';
|
||||||
@ -90,7 +90,8 @@ const SUGGESTIONS_RESPONSE = {
|
|||||||
NO_RESULT: 'No results found.',
|
NO_RESULT: 'No results found.',
|
||||||
};
|
};
|
||||||
|
|
||||||
const AutoComplete = ({
|
const AutoComplete = memo(
|
||||||
|
({
|
||||||
suggestions,
|
suggestions,
|
||||||
startAdornment,
|
startAdornment,
|
||||||
onChange,
|
onChange,
|
||||||
@ -105,7 +106,7 @@ const AutoComplete = ({
|
|||||||
suggestionsLoading = false,
|
suggestionsLoading = false,
|
||||||
suggestionsLoaded = false,
|
suggestionsLoaded = false,
|
||||||
suggestionsError = false,
|
suggestionsError = false,
|
||||||
}: Props): JSX.Element => {
|
}: Props) => {
|
||||||
const autosuggestProps = {
|
const autosuggestProps = {
|
||||||
renderInputComponent,
|
renderInputComponent,
|
||||||
suggestions,
|
suggestions,
|
||||||
@ -148,6 +149,7 @@ const AutoComplete = ({
|
|||||||
/>
|
/>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default AutoComplete;
|
export default AutoComplete;
|
||||||
|
@ -19,7 +19,6 @@ interface Props {
|
|||||||
|
|
||||||
/* eslint-disable react/jsx-max-depth */
|
/* eslint-disable react/jsx-max-depth */
|
||||||
/* eslint-disable react/jsx-no-bind*/
|
/* eslint-disable react/jsx-no-bind*/
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
|
||||||
const Header: React.FC<Props> = ({ withoutSearch }) => {
|
const Header: React.FC<Props> = ({ withoutSearch }) => {
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
const [isInfoDialogOpen, setOpenInfoDialog] = useState();
|
const [isInfoDialogOpen, setOpenInfoDialog] = useState();
|
||||||
|
@ -1,272 +1,147 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter as Router } from 'react-router-dom';
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
|
||||||
import { mount, shallow } from '../../utils/test-enzyme';
|
import { render, fireEvent, waitForElement } from '../../utils/test-react-testing-library';
|
||||||
|
import api from '../../utils/api';
|
||||||
|
|
||||||
import Search from './Search';
|
import Search from './Search';
|
||||||
|
|
||||||
const SEARCH_FILE_PATH = './Search';
|
/* eslint-disable verdaccio/jsx-spread */
|
||||||
const API_FILE_PATH = '../../utils/calls';
|
const ComponentToBeRendered: React.FC = () => (
|
||||||
const URL_FILE_PATH = '../../utils/url';
|
<Router>
|
||||||
|
|
||||||
// Global mocks
|
|
||||||
const event = {
|
|
||||||
stopPropagation: jest.fn(),
|
|
||||||
};
|
|
||||||
window.location.assign = jest.fn();
|
|
||||||
|
|
||||||
describe('<Search /> component test', () => {
|
|
||||||
let routerWrapper;
|
|
||||||
let wrapper;
|
|
||||||
beforeEach(() => {
|
|
||||||
routerWrapper = mount(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Search />
|
<Search />
|
||||||
</BrowserRouter>
|
</Router>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('<Search /> component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.resetAllMocks();
|
||||||
|
jest.spyOn(api, 'request').mockImplementation(() =>
|
||||||
|
Promise.resolve([
|
||||||
|
{
|
||||||
|
name: '@verdaccio/types',
|
||||||
|
version: '8.4.2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'verdaccio',
|
||||||
|
version: '4.3.5',
|
||||||
|
},
|
||||||
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should load the component in default state', () => {
|
test('should load the component in default state', () => {
|
||||||
expect(routerWrapper.html()).toMatchSnapshot();
|
const { container } = render(<ComponentToBeRendered />);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleSearch: when user type package name in search component, show suggestions', async () => {
|
||||||
|
const { getByPlaceholderText, getAllByText } = render(<ComponentToBeRendered />);
|
||||||
|
|
||||||
|
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||||
|
|
||||||
|
fireEvent.focus(autoCompleteInput);
|
||||||
|
fireEvent.change(autoCompleteInput, { target: { value: 'verdaccio' } });
|
||||||
|
|
||||||
|
expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
|
||||||
|
|
||||||
|
const suggestionsElements = await waitForElement(() => getAllByText('verdaccio', { exact: true }));
|
||||||
|
|
||||||
|
expect(suggestionsElements).toHaveLength(2);
|
||||||
|
expect(api.request).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('onBlur: should cancel all search requests', async () => {
|
test('onBlur: should cancel all search requests', async () => {
|
||||||
const Search = require(SEARCH_FILE_PATH).Search;
|
const { getByPlaceholderText, getByRole, getAllByText } = render(<ComponentToBeRendered />);
|
||||||
|
|
||||||
const routerWrapper = shallow(
|
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||||
<BrowserRouter>
|
|
||||||
<Search />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper = routerWrapper.find(Search).dive();
|
fireEvent.focus(autoCompleteInput);
|
||||||
const { handleOnBlur, requestList, setState } = wrapper.instance();
|
fireEvent.change(autoCompleteInput, { target: { value: 'verdaccio' } });
|
||||||
const spyCancelAllSearchRequests = jest.spyOn(wrapper.instance(), 'cancelAllSearchRequests');
|
expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
|
||||||
setState({ search: 'verdaccio' });
|
|
||||||
|
|
||||||
const request = {
|
const suggestionsElements = await waitForElement(() => getAllByText('verdaccio', { exact: true }));
|
||||||
abort: jest.fn(),
|
expect(suggestionsElements).toHaveLength(2);
|
||||||
};
|
expect(api.request).toHaveBeenCalledTimes(1);
|
||||||
// adds a request for AbortController
|
|
||||||
wrapper.instance().requestList = [request];
|
|
||||||
|
|
||||||
handleOnBlur(event);
|
fireEvent.blur(autoCompleteInput);
|
||||||
|
const listBoxElement = await waitForElement(() => getByRole('listbox'));
|
||||||
expect(request.abort).toHaveBeenCalled();
|
expect(listBoxElement).toBeEmpty();
|
||||||
expect(event.stopPropagation).toHaveBeenCalled();
|
|
||||||
expect(wrapper.state('error')).toBeFalsy();
|
|
||||||
expect(wrapper.state('loaded')).toBeFalsy();
|
|
||||||
expect(wrapper.state('loading')).toBeFalsy();
|
|
||||||
expect(spyCancelAllSearchRequests).toHaveBeenCalled();
|
|
||||||
expect(requestList).toEqual([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleSearch: when user type package name in search component and set loading to true', () => {
|
test('handleSearch: cancel all search requests when there is no value in search component with type method', async () => {
|
||||||
const Search = require(SEARCH_FILE_PATH).Search;
|
const { getByPlaceholderText, getByRole } = render(<ComponentToBeRendered />);
|
||||||
|
|
||||||
const routerWrapper = shallow(
|
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||||
<BrowserRouter>
|
|
||||||
<Search />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper = routerWrapper.find(Search).dive();
|
fireEvent.focus(autoCompleteInput);
|
||||||
|
fireEvent.change(autoCompleteInput, { target: { value: ' ', method: 'type' } });
|
||||||
const { handleSearch } = wrapper.instance();
|
expect(autoCompleteInput).toHaveAttribute('value', '');
|
||||||
const newValue = 'verdaccio';
|
const listBoxElement = await waitForElement(() => getByRole('listbox'));
|
||||||
|
expect(listBoxElement).toBeEmpty();
|
||||||
handleSearch(event, { newValue, method: 'type' });
|
expect(api.request).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
expect(event.stopPropagation).toHaveBeenCalled();
|
|
||||||
expect(wrapper.state('error')).toBeFalsy();
|
|
||||||
expect(wrapper.state('loaded')).toBeFalsy();
|
|
||||||
expect(wrapper.state('loading')).toBeTruthy();
|
|
||||||
expect(wrapper.state('search')).toEqual(newValue);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleSearch: cancel all search requests when there is no value in search component with type method', () => {
|
test('handleSearch: when method is not type method', async () => {
|
||||||
const Search = require(SEARCH_FILE_PATH).Search;
|
const { getByPlaceholderText, getByRole } = render(<ComponentToBeRendered />);
|
||||||
|
|
||||||
const routerWrapper = shallow(
|
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||||
<BrowserRouter>
|
|
||||||
<Search />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper = routerWrapper.find(Search).dive();
|
fireEvent.focus(autoCompleteInput);
|
||||||
|
fireEvent.change(autoCompleteInput, { target: { value: ' ', method: 'click' } });
|
||||||
const { handleSearch, requestList } = wrapper.instance();
|
expect(autoCompleteInput).toHaveAttribute('value', '');
|
||||||
const spy = jest.spyOn(wrapper.instance(), 'cancelAllSearchRequests');
|
const listBoxElement = await waitForElement(() => getByRole('listbox'));
|
||||||
const newValue = '';
|
expect(listBoxElement).toBeEmpty();
|
||||||
|
expect(api.request).toHaveBeenCalledTimes(0);
|
||||||
handleSearch(event, { newValue, method: 'type' });
|
|
||||||
|
|
||||||
expect(event.stopPropagation).toHaveBeenCalled();
|
|
||||||
expect(wrapper.state('error')).toBeFalsy();
|
|
||||||
expect(wrapper.state('loaded')).toBeFalsy();
|
|
||||||
expect(wrapper.state('loading')).toBeTruthy();
|
|
||||||
expect(wrapper.state('search')).toEqual(newValue);
|
|
||||||
expect(spy).toHaveBeenCalled();
|
|
||||||
expect(requestList).toEqual([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleSearch: when method is not type method', () => {
|
test('handleSearch: loading is been displayed', async () => {
|
||||||
const Search = require(SEARCH_FILE_PATH).Search;
|
const { getByPlaceholderText, getByRole, getByText } = render(<ComponentToBeRendered />);
|
||||||
|
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||||
|
|
||||||
const routerWrapper = shallow(
|
fireEvent.focus(autoCompleteInput);
|
||||||
<BrowserRouter>
|
fireEvent.change(autoCompleteInput, { target: { value: 'verdaccio' } });
|
||||||
<Search />
|
expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper = routerWrapper.find(Search).dive();
|
const loadingElement = await waitForElement(() => getByText('Loading...'));
|
||||||
|
expect(loadingElement).toBeTruthy();
|
||||||
const { handleSearch } = wrapper.instance();
|
|
||||||
const newValue = '';
|
|
||||||
|
|
||||||
handleSearch(event, { newValue, method: 'click' });
|
|
||||||
|
|
||||||
expect(event.stopPropagation).toHaveBeenCalled();
|
|
||||||
expect(wrapper.state('error')).toBeFalsy();
|
|
||||||
expect(wrapper.state('loaded')).toBeFalsy();
|
|
||||||
expect(wrapper.state('loading')).toBeFalsy();
|
|
||||||
expect(wrapper.state('search')).toEqual(newValue);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handlePackagesClearRequested: should clear suggestions', () => {
|
test('handlePackagesClearRequested: should clear suggestions', async () => {
|
||||||
const Search = require(SEARCH_FILE_PATH).Search;
|
const { getByPlaceholderText, getAllByText, getByRole } = render(<ComponentToBeRendered />);
|
||||||
|
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||||
|
|
||||||
const routerWrapper = shallow(
|
fireEvent.focus(autoCompleteInput);
|
||||||
<BrowserRouter>
|
fireEvent.change(autoCompleteInput, { target: { value: 'verdaccio' } });
|
||||||
<Search />
|
expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper = routerWrapper.find(Search).dive();
|
const suggestionsElements = await waitForElement(() => getAllByText('verdaccio', { exact: true }));
|
||||||
|
expect(suggestionsElements).toHaveLength(2);
|
||||||
|
|
||||||
const { handlePackagesClearRequested } = wrapper.instance();
|
fireEvent.change(autoCompleteInput, { target: { value: ' ' } });
|
||||||
|
const listBoxElement = await waitForElement(() => getByRole('listbox'));
|
||||||
|
expect(listBoxElement).toBeEmpty();
|
||||||
|
|
||||||
handlePackagesClearRequested();
|
expect(api.request).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
expect(wrapper.state('suggestions')).toEqual([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('<Search /> component: mocks specific tests ', () => {
|
test('handleClickSearch: should change the window location on click or return key', async () => {
|
||||||
beforeEach(() => {
|
const { getByPlaceholderText, getAllByText, getByRole } = render(<ComponentToBeRendered />);
|
||||||
jest.resetModules();
|
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||||
jest.doMock('lodash/debounce', () => {
|
|
||||||
return function debounceMock(fn) {
|
fireEvent.focus(autoCompleteInput);
|
||||||
return fn;
|
fireEvent.change(autoCompleteInput, { target: { value: 'verdaccio' } });
|
||||||
};
|
expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
|
||||||
});
|
|
||||||
});
|
const suggestionsElements = await waitForElement(() => getAllByText('verdaccio', { exact: true }));
|
||||||
|
expect(suggestionsElements).toHaveLength(2);
|
||||||
test('handleFetchPackages: should load the packages from API', async () => {
|
|
||||||
const apiResponse = [{ name: 'verdaccio' }, { name: 'verdaccio-htpasswd' }];
|
// click on the second suggestion
|
||||||
const suggestions = [{ name: 'verdaccio' }, { name: 'verdaccio-htpasswd' }];
|
fireEvent.click(suggestionsElements[1]);
|
||||||
|
const listBoxElement = await waitForElement(() => getByRole('listbox'));
|
||||||
jest.doMock(API_FILE_PATH, () => ({
|
// when the page redirects, the list box should be empty again
|
||||||
callSearch(url: string) {
|
expect(listBoxElement).toBeEmpty();
|
||||||
return Promise.resolve(apiResponse);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const Search = require(SEARCH_FILE_PATH).Search;
|
|
||||||
|
|
||||||
const routerWrapper = shallow(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Search />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper = routerWrapper.find(Search).dive();
|
|
||||||
wrapper.setState({ search: 'verdaccio' });
|
|
||||||
const { handleFetchPackages } = wrapper.instance();
|
|
||||||
|
|
||||||
await handleFetchPackages({ value: 'verdaccio' });
|
|
||||||
|
|
||||||
expect(wrapper.state('suggestions')).toEqual(suggestions);
|
|
||||||
expect(wrapper.state('error')).toBeFalsy();
|
|
||||||
expect(wrapper.state('loaded')).toBeTruthy();
|
|
||||||
expect(wrapper.state('loading')).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handleFetchPackages: when browser cancel a request', async () => {
|
|
||||||
const apiResponse = { name: 'AbortError' };
|
|
||||||
|
|
||||||
jest.doMock(API_FILE_PATH, () => ({ callSearch: jest.fn(() => Promise.reject(apiResponse)) }));
|
|
||||||
|
|
||||||
const Search = require(SEARCH_FILE_PATH).Search;
|
|
||||||
|
|
||||||
const routerWrapper = shallow(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Search />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper = routerWrapper.find(Search).dive();
|
|
||||||
|
|
||||||
const { handleFetchPackages, setState } = wrapper.instance();
|
|
||||||
setState({ search: 'verdaccio' });
|
|
||||||
await handleFetchPackages({ value: 'verdaccio' });
|
|
||||||
|
|
||||||
expect(wrapper.state('error')).toBeFalsy();
|
|
||||||
expect(wrapper.state('loaded')).toBeFalsy();
|
|
||||||
expect(wrapper.state('loading')).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handleFetchPackages: when API server failed request', async () => {
|
|
||||||
const apiResponse = { name: 'BAD_REQUEST' };
|
|
||||||
|
|
||||||
jest.doMock(API_FILE_PATH, () => ({
|
|
||||||
callSearch(url) {
|
|
||||||
return Promise.reject(apiResponse);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const Search = require(SEARCH_FILE_PATH).Search;
|
|
||||||
const routerWrapper = shallow(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Search />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper = routerWrapper.find(Search).dive();
|
|
||||||
wrapper.setState({ search: 'verdaccio' });
|
|
||||||
const { handleFetchPackages } = wrapper.instance();
|
|
||||||
|
|
||||||
await handleFetchPackages({ value: 'verdaccio' });
|
|
||||||
|
|
||||||
expect(wrapper.state('error')).toBeTruthy();
|
|
||||||
expect(wrapper.state('loaded')).toBeFalsy();
|
|
||||||
expect(wrapper.state('loading')).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handleClickSearch: should change the window location on click or return key', () => {
|
|
||||||
const getDetailPageURL = jest.fn(() => 'detail/page/url');
|
|
||||||
jest.doMock(URL_FILE_PATH, () => ({ getDetailPageURL }));
|
|
||||||
|
|
||||||
const suggestionValue = [];
|
|
||||||
const Search = require(SEARCH_FILE_PATH).Search;
|
|
||||||
const pushHandler = jest.fn();
|
|
||||||
const routerWrapper = shallow(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Search history={{ push: pushHandler }} />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper = routerWrapper.find(Search).dive();
|
|
||||||
const { handleClickSearch } = wrapper.instance();
|
|
||||||
|
|
||||||
// click
|
|
||||||
handleClickSearch(event, { suggestionValue, method: 'click' });
|
|
||||||
expect(event.stopPropagation).toHaveBeenCalled();
|
|
||||||
expect(pushHandler).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
// return key
|
|
||||||
handleClickSearch(event, { suggestionValue, method: 'enter' });
|
|
||||||
expect(event.stopPropagation).toHaveBeenCalled();
|
|
||||||
expect(pushHandler).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,32 +1,12 @@
|
|||||||
import React, { KeyboardEvent, Component, ReactElement } from 'react';
|
import React, { useState, FormEvent, useCallback } from 'react';
|
||||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
|
||||||
import { SuggestionSelectedEventData, ChangeEvent } from 'react-autosuggest';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { default as IconSearch } from '@material-ui/icons/Search';
|
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
import { RouteComponentProps, withRouter } from 'react-router';
|
||||||
|
import { SuggestionSelectedEventData } from 'react-autosuggest';
|
||||||
|
|
||||||
import InputAdornment from '../../muiComponents/InputAdornment';
|
|
||||||
import AutoComplete from '../AutoComplete';
|
import AutoComplete from '../AutoComplete';
|
||||||
import { callSearch } from '../../utils/calls';
|
import { callSearch } from '../../utils/calls';
|
||||||
import { Theme } from '../../design-tokens/theme';
|
|
||||||
|
|
||||||
export interface State {
|
import SearchAdornment from './SearchAdornment';
|
||||||
search: string;
|
|
||||||
suggestions: unknown[];
|
|
||||||
loading: boolean;
|
|
||||||
loaded: boolean;
|
|
||||||
error: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type cancelAllSearchRequests = () => void;
|
|
||||||
export type handlePackagesClearRequested = () => void;
|
|
||||||
export type handleSearch = (event: React.FormEvent<HTMLInputElement>, { newValue, method }: ChangeEvent) => void;
|
|
||||||
export type handleClickSearch = (
|
|
||||||
event: KeyboardEvent<HTMLInputElement>,
|
|
||||||
{ suggestionValue, method }: { suggestionValue: object[]; method: string }
|
|
||||||
) => void;
|
|
||||||
export type handleFetchPackages = ({ value: string }) => Promise<void>;
|
|
||||||
export type onBlur = (event: React.FormEvent<HTMLInputElement>) => void;
|
|
||||||
|
|
||||||
const CONSTANTS = {
|
const CONSTANTS = {
|
||||||
API_DELAY: 300,
|
API_DELAY: 300,
|
||||||
@ -34,38 +14,135 @@ const CONSTANTS = {
|
|||||||
ABORT_ERROR: 'AbortError',
|
ABORT_ERROR: 'AbortError',
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledInputAdornment = styled(InputAdornment)<{ theme?: Theme }>(props => ({
|
const Search: React.FC<RouteComponentProps> = ({ history }) => {
|
||||||
color: props.theme && props.theme.palette.white,
|
const [suggestions, setSuggestions] = useState([]);
|
||||||
}));
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [requestList, setRequestList] = useState<Array<{ abort: () => void }>>([]);
|
||||||
|
|
||||||
export class Search extends Component<RouteComponentProps<{}>, State> {
|
/**
|
||||||
constructor(props: RouteComponentProps<{}>) {
|
* Cancel all the requests which are in pending state.
|
||||||
super(props);
|
*/
|
||||||
this.state = {
|
const cancelAllSearchRequests = useCallback(() => {
|
||||||
search: '',
|
requestList.forEach(request => request.abort());
|
||||||
suggestions: [],
|
setRequestList([]);
|
||||||
// loading: A boolean value to indicate that request is in pending state.
|
}, [requestList, setRequestList]);
|
||||||
loading: false,
|
|
||||||
// loaded: A boolean value to indicate that result has been loaded.
|
/**
|
||||||
loaded: false,
|
* As user focuses out from input, we cancel all the request from requestList
|
||||||
// error: A boolean value to indicate API error.
|
* and set the API state parameters to default boolean values.
|
||||||
error: false,
|
*/
|
||||||
};
|
const handleOnBlur = useCallback(
|
||||||
this.requestList = [];
|
(event: FormEvent<HTMLInputElement>) => {
|
||||||
|
// stops event bubbling
|
||||||
|
event.stopPropagation();
|
||||||
|
setLoaded(false);
|
||||||
|
setLoading(false);
|
||||||
|
setError(false);
|
||||||
|
cancelAllSearchRequests();
|
||||||
|
},
|
||||||
|
[setLoaded, setLoading, cancelAllSearchRequests, setError]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onChange method for the input element.
|
||||||
|
*/
|
||||||
|
const handleSearch = useCallback(
|
||||||
|
(event: FormEvent<HTMLInputElement>, { newValue, method }) => {
|
||||||
|
// stops event bubbling
|
||||||
|
event.stopPropagation();
|
||||||
|
if (method === 'type') {
|
||||||
|
const value = newValue.trim();
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
|
setSearch(value);
|
||||||
|
setLoaded(false);
|
||||||
|
/**
|
||||||
|
* A use case where User keeps adding and removing value in input field,
|
||||||
|
* so we cancel all the existing requests when input is empty.
|
||||||
|
*/
|
||||||
|
if (value.length === 0) {
|
||||||
|
cancelAllSearchRequests();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[cancelAllSearchRequests]
|
||||||
|
);
|
||||||
|
|
||||||
public render(): ReactElement<HTMLElement> {
|
/**
|
||||||
const { suggestions, search, loaded, loading, error } = this.state;
|
* Cancel all the request from list and make request list empty.
|
||||||
|
*/
|
||||||
|
const handlePackagesClearRequested = useCallback(() => {
|
||||||
|
setSuggestions([]);
|
||||||
|
}, [setSuggestions]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When an user select any package by clicking or pressing return key.
|
||||||
|
*/
|
||||||
|
const handleClickSearch = useCallback(
|
||||||
|
(
|
||||||
|
event: FormEvent<HTMLInputElement>,
|
||||||
|
{ suggestionValue, method }: SuggestionSelectedEventData<unknown>
|
||||||
|
): void | undefined => {
|
||||||
|
// stops event bubbling
|
||||||
|
event.stopPropagation();
|
||||||
|
switch (method) {
|
||||||
|
case 'click':
|
||||||
|
case 'enter':
|
||||||
|
setSearch('');
|
||||||
|
history.push(`/-/web/detail/${suggestionValue}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[history]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch packages from API.
|
||||||
|
* For AbortController see: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
|
||||||
|
*/
|
||||||
|
const handleFetchPackages = useCallback(
|
||||||
|
async ({ value }: { value: string }) => {
|
||||||
|
try {
|
||||||
|
const controller = new window.AbortController();
|
||||||
|
const signal = controller.signal;
|
||||||
|
// Keep track of search requests.
|
||||||
|
setRequestList([...requestList, controller]);
|
||||||
|
const suggestions = await callSearch(value, signal);
|
||||||
|
// @ts-ignore FIXME: Argument of type 'unknown' is not assignable to parameter of type 'SetStateAction<never[]>'
|
||||||
|
setSuggestions(suggestions);
|
||||||
|
setLoaded(true);
|
||||||
|
} catch (error) {
|
||||||
|
/**
|
||||||
|
* AbortError is not the API error.
|
||||||
|
* It means browser has cancelled the API request.
|
||||||
|
*/
|
||||||
|
if (error.name === CONSTANTS.ABORT_ERROR) {
|
||||||
|
setError(false);
|
||||||
|
setLoaded(false);
|
||||||
|
} else {
|
||||||
|
setError(true);
|
||||||
|
setLoaded(false);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[requestList, setRequestList, setSuggestions, setLoaded, setError, setLoading]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
onBlur={this.handleOnBlur}
|
onBlur={handleOnBlur}
|
||||||
onChange={this.handleSearch}
|
onChange={handleSearch}
|
||||||
onCleanSuggestions={this.handlePackagesClearRequested}
|
onCleanSuggestions={handlePackagesClearRequested}
|
||||||
onClick={this.handleClickSearch}
|
onClick={handleClickSearch}
|
||||||
onSuggestionsFetch={debounce(this.handleFetchPackages, CONSTANTS.API_DELAY)}
|
onSuggestionsFetch={debounce(handleFetchPackages, CONSTANTS.API_DELAY)}
|
||||||
placeholder={CONSTANTS.PLACEHOLDER_TEXT}
|
placeholder={CONSTANTS.PLACEHOLDER_TEXT}
|
||||||
startAdornment={this.getAdorment()}
|
startAdornment={<SearchAdornment />}
|
||||||
suggestions={suggestions}
|
suggestions={suggestions}
|
||||||
suggestionsError={error}
|
suggestionsError={error}
|
||||||
suggestionsLoaded={loaded}
|
suggestionsLoaded={loaded}
|
||||||
@ -73,129 +150,6 @@ export class Search extends Component<RouteComponentProps<{}>, State> {
|
|||||||
value={search}
|
value={search}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel all the requests which are in pending state.
|
|
||||||
*/
|
|
||||||
private cancelAllSearchRequests: cancelAllSearchRequests = () => {
|
|
||||||
this.requestList.forEach(request => request.abort());
|
|
||||||
this.requestList = [];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel all the request from list and make request list empty.
|
|
||||||
*/
|
|
||||||
private handlePackagesClearRequested: handlePackagesClearRequested = () => {
|
|
||||||
this.setState({
|
|
||||||
suggestions: [],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onChange method for the input element.
|
|
||||||
*/
|
|
||||||
private handleSearch: handleSearch = (event, { newValue, method }) => {
|
|
||||||
// stops event bubbling
|
|
||||||
event.stopPropagation();
|
|
||||||
if (method === 'type') {
|
|
||||||
const value = newValue.trim();
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
search: value,
|
|
||||||
loading: true,
|
|
||||||
loaded: false,
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
/**
|
|
||||||
* A use case where User keeps adding and removing value in input field,
|
|
||||||
* so we cancel all the existing requests when input is empty.
|
|
||||||
*/
|
|
||||||
if (value.length === 0) {
|
|
||||||
this.cancelAllSearchRequests();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When an user select any package by clicking or pressing return key.
|
|
||||||
*/
|
|
||||||
private handleClickSearch = (
|
|
||||||
event: React.FormEvent<HTMLInputElement>,
|
|
||||||
{ suggestionValue, method }: SuggestionSelectedEventData<unknown>
|
|
||||||
): void | undefined => {
|
|
||||||
const { history } = this.props;
|
|
||||||
// stops event bubbling
|
|
||||||
event.stopPropagation();
|
|
||||||
switch (method) {
|
|
||||||
case 'click':
|
|
||||||
case 'enter':
|
|
||||||
this.setState({ search: '' });
|
|
||||||
history.push(`/-/web/detail/${suggestionValue}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch packages from API.
|
|
||||||
* For AbortController see: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
|
|
||||||
*/
|
|
||||||
private handleFetchPackages: handleFetchPackages = async ({ value }) => {
|
|
||||||
try {
|
|
||||||
const controller = new window.AbortController();
|
|
||||||
const signal = controller.signal;
|
|
||||||
// Keep track of search requests.
|
|
||||||
this.requestList.push(controller);
|
|
||||||
const suggestions = await callSearch(value, signal);
|
|
||||||
// @ts-ignore
|
|
||||||
this.setState({
|
|
||||||
suggestions,
|
|
||||||
loaded: true,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
/**
|
|
||||||
* AbortError is not the API error.
|
|
||||||
* It means browser has cancelled the API request.
|
|
||||||
*/
|
|
||||||
if (error.name === CONSTANTS.ABORT_ERROR) {
|
|
||||||
this.setState({ error: false, loaded: false });
|
|
||||||
} else {
|
|
||||||
this.setState({ error: true, loaded: false });
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.setState({ loading: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private requestList: AbortController[];
|
|
||||||
|
|
||||||
public getAdorment(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<StyledInputAdornment position={'start'}>
|
|
||||||
<IconSearch />
|
|
||||||
</StyledInputAdornment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* As user focuses out from input, we cancel all the request from requestList
|
|
||||||
* and set the API state parameters to default boolean values.
|
|
||||||
*/
|
|
||||||
private handleOnBlur: onBlur = event => {
|
|
||||||
// stops event bubbling
|
|
||||||
event.stopPropagation();
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
loaded: false,
|
|
||||||
loading: false,
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
() => this.cancelAllSearchRequests()
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(Search);
|
export default withRouter(Search);
|
||||||
|
18
src/components/Search/SearchAdornment.tsx
Normal file
18
src/components/Search/SearchAdornment.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Search from '@material-ui/icons/Search';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import InputAdornment from '../../muiComponents/InputAdornment';
|
||||||
|
import { Theme } from '../../design-tokens/theme';
|
||||||
|
|
||||||
|
const StyledInputAdornment = styled(InputAdornment)<{ theme?: Theme }>(props => ({
|
||||||
|
color: props.theme && props.theme.palette.white,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const SearchAdornment: React.FC = () => (
|
||||||
|
<StyledInputAdornment position={'start'}>
|
||||||
|
<Search />
|
||||||
|
</StyledInputAdornment>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SearchAdornment;
|
@ -1,3 +1,87 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`<Search /> component test should load the component in default state 1`] = `"<div class=\\"css-pnwf4z-Wrapper e1rflf270\\"><div role=\\"combobox\\" aria-haspopup=\\"listbox\\" aria-owns=\\"react-autowhatever-1\\" aria-expanded=\\"false\\" class=\\"react-autosuggest__container\\"><div class=\\"MuiFormControl-root MuiTextField-root react-autosuggest__input css-ae5nkp-StyledTextField e1rflf271 MuiFormControl-fullWidth\\" aria-autocomplete=\\"list\\" aria-controls=\\"react-autowhatever-1\\"><div class=\\"MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-fullWidth MuiInput-fullWidth MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedStart\\"><div class=\\"MuiInputAdornment-root css-1wub48n-StyledInputAdornment e1n3ivvz0 MuiInputAdornment-positionStart\\"><svg class=\\"MuiSvgIcon-root\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path d=\\"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z\\"></path></svg></div><input aria-invalid=\\"false\\" autocomplete=\\"off\\" placeholder=\\"Search Packages\\" type=\\"text\\" class=\\"MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedStart\\" value=\\"\\"></div></div><div class=\\"MuiPaper-root MuiPaper-elevation1 react-autosuggest__suggestions-container css-kc7cda-SuggestionContainer e1rflf272\\" id=\\"react-autowhatever-1\\" role=\\"listbox\\"></div></div></div>"`;
|
exports[`<Search /> component should load the component in default state 1`] = `
|
||||||
|
.emotion-6 {
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-2 .MuiInputBase-root:before {
|
||||||
|
content: '';
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-2 .MuiInputBase-root:after {
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-2 .MuiInputBase-root:hover:before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-2 .MuiInputBase-input {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-0 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-4 {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="emotion-6 emotion-7"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-owns="react-autowhatever-1"
|
||||||
|
class="react-autosuggest__container"
|
||||||
|
role="combobox"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-controls="react-autowhatever-1"
|
||||||
|
class="MuiFormControl-root MuiTextField-root react-autosuggest__input emotion-2 emotion-3 MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-fullWidth MuiInput-fullWidth MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedStart"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiInputAdornment-root emotion-0 emotion-1 MuiInputAdornment-positionStart"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root"
|
||||||
|
focusable="false"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
autocomplete="off"
|
||||||
|
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedStart"
|
||||||
|
placeholder="Search Packages"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiPaper-root MuiPaper-elevation1 react-autosuggest__suggestions-container emotion-4 emotion-5"
|
||||||
|
id="react-autowhatever-1"
|
||||||
|
role="listbox"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"rules": {
|
"rules": {
|
||||||
"verdaccio/jsx-spread": 0,
|
"verdaccio/jsx-spread": 0,
|
||||||
"react/display-name": 0,
|
|
||||||
"react/jsx-sort-props": 0
|
"react/jsx-sort-props": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,6 @@ import { mount, shallow } from 'enzyme';
|
|||||||
|
|
||||||
import ThemeProvider from '../design-tokens/ThemeProvider';
|
import ThemeProvider from '../design-tokens/ThemeProvider';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
|
||||||
|
|
||||||
const shallowWithTheme = (element: React.ReactElement<any>, ...props): any =>
|
const shallowWithTheme = (element: React.ReactElement<any>, ...props): any =>
|
||||||
shallow(element, {
|
shallow(element, {
|
||||||
wrappingComponent: ThemeProvider,
|
wrappingComponent: ThemeProvider,
|
||||||
|
@ -3,7 +3,6 @@ import React from 'react';
|
|||||||
|
|
||||||
import ThemeProvider from '../design-tokens/ThemeProvider';
|
import ThemeProvider from '../design-tokens/ThemeProvider';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
|
||||||
const customRender = (node: React.ReactElement<any>, ...options: Array<any>) => {
|
const customRender = (node: React.ReactElement<any>, ...options: Array<any>) => {
|
||||||
return render(<ThemeProvider>{node}</ThemeProvider>, ...options);
|
return render(<ThemeProvider>{node}</ThemeProvider>, ...options);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-var-requires": 0,
|
"@typescript-eslint/no-var-requires": 0,
|
||||||
"@typescript-eslint/explicit-function-return-type": 0,
|
|
||||||
"@typescript-eslint/no-explicit-any": 0,
|
"@typescript-eslint/no-explicit-any": 0,
|
||||||
"@typescript-eslint/explicit-member-accessibility": 0,
|
"@typescript-eslint/explicit-member-accessibility": 0,
|
||||||
"no-console": 0,
|
"no-console": 0,
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"src",
|
"src",
|
||||||
"types/*.d.ts",
|
"types/*.d.ts",
|
||||||
"scripts/lib",
|
"scripts/lib",
|
||||||
"node_modules/config"
|
"node_modules/config",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
|
1
types/jest-dom.d.ts
vendored
Normal file
1
types/jest-dom.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom/extend-expect';
|
55
yarn.lock
55
yarn.lock
@ -1071,6 +1071,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.2"
|
regenerator-runtime "^0.13.2"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.5.1":
|
||||||
|
version "7.7.4"
|
||||||
|
resolved "https://registry.verdaccio.org/@babel%2fruntime/-/runtime-7.7.4.tgz#b23a856751e4bf099262f867767889c0e3fe175b"
|
||||||
|
integrity sha512-r24eVUUr0QqNZa+qrImUk8fn5SPhHq+IfYvIoIMg0do3GdK9sMdiLKP3GYVVaxpPKORgm8KRKaNTEhAjgIpLMw==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.2"
|
||||||
|
|
||||||
"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0":
|
"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0":
|
||||||
version "7.6.0"
|
version "7.6.0"
|
||||||
resolved "https://registry.verdaccio.org/@babel%2ftemplate/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6"
|
resolved "https://registry.verdaccio.org/@babel%2ftemplate/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6"
|
||||||
@ -1739,6 +1746,21 @@
|
|||||||
pretty-format "^24.8.0"
|
pretty-format "^24.8.0"
|
||||||
wait-for-expect "^1.3.0"
|
wait-for-expect "^1.3.0"
|
||||||
|
|
||||||
|
"@testing-library/jest-dom@4.2.4":
|
||||||
|
version "4.2.4"
|
||||||
|
resolved "https://registry.verdaccio.org/@testing-library%2fjest-dom/-/jest-dom-4.2.4.tgz#00dfa0cbdd837d9a3c2a7f3f0a248ea6e7b89742"
|
||||||
|
integrity sha512-j31Bn0rQo12fhCWOUWy9fl7wtqkp7In/YP2p5ZFyRuiiB9Qs3g+hS4gAmDWONbAHcRmVooNJ5eOHQDCOmUFXHg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.5.1"
|
||||||
|
chalk "^2.4.1"
|
||||||
|
css "^2.2.3"
|
||||||
|
css.escape "^1.5.1"
|
||||||
|
jest-diff "^24.0.0"
|
||||||
|
jest-matcher-utils "^24.0.0"
|
||||||
|
lodash "^4.17.11"
|
||||||
|
pretty-format "^24.0.0"
|
||||||
|
redent "^3.0.0"
|
||||||
|
|
||||||
"@testing-library/react@9.3.2":
|
"@testing-library/react@9.3.2":
|
||||||
version "9.3.2"
|
version "9.3.2"
|
||||||
resolved "https://registry.verdaccio.org/@testing-library%2freact/-/react-9.3.2.tgz#418000daa980dafd2d9420cc733d661daece9aa0"
|
resolved "https://registry.verdaccio.org/@testing-library%2freact/-/react-9.3.2.tgz#418000daa980dafd2d9420cc733d661daece9aa0"
|
||||||
@ -4549,7 +4571,12 @@ css-what@2.1, css-what@^2.1.2:
|
|||||||
resolved "https://registry.verdaccio.org/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
|
resolved "https://registry.verdaccio.org/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
|
||||||
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
|
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
|
||||||
|
|
||||||
css@^2.0.0, css@^2.2.1:
|
css.escape@^1.5.1:
|
||||||
|
version "1.5.1"
|
||||||
|
resolved "https://registry.verdaccio.org/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
|
||||||
|
integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=
|
||||||
|
|
||||||
|
css@^2.0.0, css@^2.2.1, css@^2.2.3:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.verdaccio.org/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
|
resolved "https://registry.verdaccio.org/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
|
||||||
integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==
|
integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==
|
||||||
@ -7944,7 +7971,7 @@ jest-config@^24.9.0:
|
|||||||
pretty-format "^24.9.0"
|
pretty-format "^24.9.0"
|
||||||
realpath-native "^1.1.0"
|
realpath-native "^1.1.0"
|
||||||
|
|
||||||
jest-diff@^24.3.0, jest-diff@^24.9.0:
|
jest-diff@^24.0.0, jest-diff@^24.3.0, jest-diff@^24.9.0:
|
||||||
version "24.9.0"
|
version "24.9.0"
|
||||||
resolved "https://registry.verdaccio.org/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da"
|
resolved "https://registry.verdaccio.org/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da"
|
||||||
integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==
|
integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==
|
||||||
@ -8072,7 +8099,7 @@ jest-leak-detector@^24.9.0:
|
|||||||
jest-get-type "^24.9.0"
|
jest-get-type "^24.9.0"
|
||||||
pretty-format "^24.9.0"
|
pretty-format "^24.9.0"
|
||||||
|
|
||||||
jest-matcher-utils@^24.9.0:
|
jest-matcher-utils@^24.0.0, jest-matcher-utils@^24.9.0:
|
||||||
version "24.9.0"
|
version "24.9.0"
|
||||||
resolved "https://registry.verdaccio.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073"
|
resolved "https://registry.verdaccio.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073"
|
||||||
integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==
|
integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==
|
||||||
@ -9434,6 +9461,11 @@ min-document@^2.19.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
dom-walk "^0.1.0"
|
dom-walk "^0.1.0"
|
||||||
|
|
||||||
|
min-indent@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.verdaccio.org/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256"
|
||||||
|
integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=
|
||||||
|
|
||||||
mini-create-react-context@^0.3.0:
|
mini-create-react-context@^0.3.0:
|
||||||
version "0.3.2"
|
version "0.3.2"
|
||||||
resolved "https://registry.verdaccio.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189"
|
resolved "https://registry.verdaccio.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189"
|
||||||
@ -11178,7 +11210,7 @@ pretty-error@^2.0.2:
|
|||||||
renderkid "^2.0.1"
|
renderkid "^2.0.1"
|
||||||
utila "~0.4"
|
utila "~0.4"
|
||||||
|
|
||||||
pretty-format@^24.3.0, pretty-format@^24.8.0, pretty-format@^24.9.0:
|
pretty-format@^24.0.0, pretty-format@^24.3.0, pretty-format@^24.8.0, pretty-format@^24.9.0:
|
||||||
version "24.9.0"
|
version "24.9.0"
|
||||||
resolved "https://registry.verdaccio.org/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9"
|
resolved "https://registry.verdaccio.org/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9"
|
||||||
integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==
|
integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==
|
||||||
@ -11715,6 +11747,14 @@ redent@^2.0.0:
|
|||||||
indent-string "^3.0.0"
|
indent-string "^3.0.0"
|
||||||
strip-indent "^2.0.0"
|
strip-indent "^2.0.0"
|
||||||
|
|
||||||
|
redent@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.verdaccio.org/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
|
||||||
|
integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
|
||||||
|
dependencies:
|
||||||
|
indent-string "^4.0.0"
|
||||||
|
strip-indent "^3.0.0"
|
||||||
|
|
||||||
reflect.ownkeys@^0.2.0:
|
reflect.ownkeys@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.verdaccio.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
|
resolved "https://registry.verdaccio.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
|
||||||
@ -13029,6 +13069,13 @@ strip-indent@^2.0.0:
|
|||||||
resolved "https://registry.verdaccio.org/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
|
resolved "https://registry.verdaccio.org/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
|
||||||
integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
|
integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
|
||||||
|
|
||||||
|
strip-indent@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.verdaccio.org/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
|
||||||
|
integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
|
||||||
|
dependencies:
|
||||||
|
min-indent "^1.0.0"
|
||||||
|
|
||||||
strip-json-comments@2.0.1, strip-json-comments@~2.0.1:
|
strip-json-comments@2.0.1, strip-json-comments@~2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.verdaccio.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
resolved "https://registry.verdaccio.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||||
|
Loading…
Reference in New Issue
Block a user