diff --git a/Makefile b/Makefile index 3f2b1b0fb4..cad1dd7ad1 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,7 @@ react-app-lint-fix: .PHONY: react-app-test react-app-test: | $(REACT_APP_NODE_MODULES_PATH) react-app-lint @echo ">> running React app tests" - cd $(REACT_APP_PATH) && yarn test --no-watch + cd $(REACT_APP_PATH) && yarn test --no-watch --coverage .PHONY: test test: common-test react-app-test diff --git a/web/ui/react-app/package.json b/web/ui/react-app/package.json index 0f2296b47c..770bdcd4fe 100644 --- a/web/ui/react-app/package.json +++ b/web/ui/react-app/package.json @@ -7,7 +7,7 @@ "@fortawesome/free-solid-svg-icons": "^5.7.1", "@fortawesome/react-fontawesome": "^0.1.4", "@reach/router": "^1.2.1", - "@types/jest": "^24.0.4", + "@types/jest": "^24.0.20", "@types/jquery": "^3.3.29", "@types/node": "^12.11.1", "@types/reach__router": "^1.2.6", @@ -42,6 +42,7 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", + "test:debug": "react-scripts --inspect-brk test --runInBand --no-cache", "eject": "react-scripts eject", "lint:ci": "eslint --quiet \"src/**/*.{ts,tsx}\"", "lint": "eslint --fix \"src/**/*.{ts,tsx}\"" @@ -58,6 +59,8 @@ "not op_mini all" ], "devDependencies": { + "@types/enzyme": "^3.10.3", + "@types/enzyme-adapter-react-16": "^1.0.5", "@types/flot": "0.0.31", "@types/moment-timezone": "^0.5.10", "@types/reactstrap": "^8.0.5", @@ -73,7 +76,11 @@ "eslint-plugin-prettier": "^3.1.1", "eslint-plugin-react": "7.x", "eslint-plugin-react-hooks": "1.x", - "prettier": "^1.18.2" + "prettier": "^1.18.2", + "@types/sinon": "^7.5.0", + "enzyme": "^3.10.0", + "enzyme-adapter-react-16": "^1.15.1", + "sinon": "^7.5.0" }, "proxy": "http://localhost:9090" } diff --git a/web/ui/react-app/src/App.test.js b/web/ui/react-app/src/App.test.js deleted file mode 100755 index 4378d2a0a7..0000000000 --- a/web/ui/react-app/src/App.test.js +++ /dev/null @@ -1,10 +0,0 @@ -import './globals'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); -}); diff --git a/web/ui/react-app/src/App.test.tsx b/web/ui/react-app/src/App.test.tsx new file mode 100755 index 0000000000..621a52e539 --- /dev/null +++ b/web/ui/react-app/src/App.test.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import App from './App'; +import Navigation from './Navbar'; +import { Container } from 'reactstrap'; +import { Router } from '@reach/router'; +import { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList } from './pages'; + +describe('App', () => { + const app = shallow(); + + it('navigates', () => { + expect(app.find(Navigation)).toHaveLength(1); + }); + it('routes', () => { + [Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList].forEach(component => + expect(app.find(component)).toHaveLength(1) + ); + expect(app.find(Router)).toHaveLength(1); + expect(app.find(Container)).toHaveLength(1); + }); +}); diff --git a/web/ui/react-app/src/Checkbox.test.tsx b/web/ui/react-app/src/Checkbox.test.tsx new file mode 100755 index 0000000000..52258f72dd --- /dev/null +++ b/web/ui/react-app/src/Checkbox.test.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Checkbox from './Checkbox'; +import { FormGroup, Label, Input } from 'reactstrap'; + +const MockCmp: React.FC = () =>
; + +describe('Checkbox', () => { + it('renders with subcomponents', () => { + const checkBox = shallow(); + [FormGroup, Input, Label].forEach(component => expect(checkBox.find(component)).toHaveLength(1)); + }); + + it('passes down the correct FormGroup props', () => { + const checkBoxProps = { wrapperStyles: { color: 'orange' } }; + const checkBox = shallow(); + const formGroup = checkBox.find(FormGroup); + expect(Object.keys(formGroup.props())).toHaveLength(4); + expect(formGroup.prop('className')).toEqual('custom-control custom-checkbox'); + expect(formGroup.prop('children')).toHaveLength(2); + expect(formGroup.prop('style')).toEqual({ color: 'orange' }); + expect(formGroup.prop('tag')).toEqual('div'); + }); + + it('passes down the correct FormGroup Input props', () => { + const results: string[] = []; + const checkBoxProps = { + onChange: (): void => { + results.push('clicked'); + }, + }; + const checkBox = shallow(); + const input = checkBox.find(Input); + expect(Object.keys(input.props())).toHaveLength(4); + expect(input.prop('className')).toEqual('custom-control-input'); + expect(input.prop('id')).toMatch('1'); + expect(input.prop('type')).toEqual('checkbox'); + input.simulate('change'); + expect(results).toHaveLength(1); + expect(results[0]).toEqual('clicked'); + }); + + it('passes down the correct Label props', () => { + const checkBox = shallow( + + + + ); + const label = checkBox.find(Label); + expect(Object.keys(label.props())).toHaveLength(6); + expect(label.prop('className')).toEqual('custom-control-label'); + expect(label.find(MockCmp)).toHaveLength(1); + expect(label.prop('for')).toMatch('1'); + expect(label.prop('style')).toEqual({ userSelect: 'none' }); + expect(label.prop('tag')).toEqual('label'); + }); + + it('shares checkbox `id` uuid with Input/Label subcomponents', () => { + const checkBox = shallow(); + const input = checkBox.find(Input); + const label = checkBox.find(Label); + expect(label.prop('for')).toBeDefined(); + expect(label.prop('for')).toEqual(input.prop('id')); + }); +}); diff --git a/web/ui/react-app/src/DataTable.test.tsx b/web/ui/react-app/src/DataTable.test.tsx new file mode 100755 index 0000000000..6803406c5d --- /dev/null +++ b/web/ui/react-app/src/DataTable.test.tsx @@ -0,0 +1,267 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import DataTable, { QueryResult } from './DataTable'; +import { Alert, Table } from 'reactstrap'; +import SeriesName from './SeriesName'; + +describe('DataTable', () => { + describe('when data is null', () => { + it('renders an alert', () => { + const table = shallow(); + const alert = table.find(Alert); + expect(Object.keys(alert.props())).toHaveLength(7); + expect(alert.prop('color')).toEqual('light'); + expect(alert.prop('children')).toEqual('No data queried yet'); + }); + }); + + describe('when data.result is empty', () => { + it('renders an alert', () => { + const dataTableProps: QueryResult = { + data: { + resultType: 'vector', + result: [], + }, + }; + const table = shallow(); + const alert = table.find(Alert); + expect(Object.keys(alert.props())).toHaveLength(7); + expect(alert.prop('color')).toEqual('secondary'); + expect(alert.prop('children')).toEqual('Empty query result'); + }); + }); + + describe('when resultType is a vector with values', () => { + const dataTableProps: QueryResult = { + data: { + resultType: 'vector', + result: [ + { + metric: { + __name__: 'metric_name_1', + label1: 'value_1', + labeln: 'value_n', + }, + value: [1572098246.599, '0'], + }, + { + metric: { + __name__: 'metric_name_2', + label1: 'value_1', + labeln: 'value_n', + }, + value: [1572098246.599, '1'], + }, + ], + }, + }; + const dataTable = shallow(); + + it('renders a table', () => { + const table = dataTable.find(Table); + expect(table.prop('hover')).toBe(true); + expect(table.prop('size')).toEqual('sm'); + expect(table.prop('className')).toEqual('data-table'); + expect(table.find('tbody')).toHaveLength(1); + }); + + it('renders rows', () => { + const table = dataTable.find(Table); + table.find('tr').forEach((row, idx) => { + expect(row.find(SeriesName)).toHaveLength(1); + expect( + row + .find('td') + .at(1) + .text() + ).toEqual(`${idx}`); + }); + }); + }); + + describe('when resultType is a vector with too many values', () => { + const dataTableProps: QueryResult = { + data: { + resultType: 'vector', + result: Array.from(Array(10001).keys()).map(i => { + return { + metric: { + __name__: `metric_name_${i}`, + label1: 'value_1', + labeln: 'value_n', + }, + value: [1572098246.599, `${i}`], + }; + }), + }, + }; + const dataTable = shallow(); + + it('renders limited rows', () => { + const table = dataTable.find(Table); + expect(table.find('tr')).toHaveLength(10000); + }); + + it('renders a warning', () => { + const alert = dataTable.find(Alert); + expect(alert.render().text()).toEqual('Warning: Fetched 10001 metrics, only displaying first 10000.'); + }); + }); + + describe('when result type is a matrix', () => { + const dataTableProps: QueryResult = { + data: { + resultType: 'matrix', + result: [ + { + metric: { + __name__: 'promhttp_metric_handler_requests_total', + code: '200', + instance: 'localhost:9090', + job: 'prometheus', + }, + values: [ + [1572097950.93, '9'], + [1572097965.931, '10'], + [1572097980.929, '11'], + [1572097995.931, '12'], + [1572098010.932, '13'], + [1572098025.933, '14'], + [1572098040.93, '15'], + [1572098055.93, '16'], + [1572098070.93, '17'], + [1572098085.936, '18'], + [1572098100.936, '19'], + [1572098115.933, '20'], + [1572098130.932, '21'], + [1572098145.932, '22'], + [1572098160.933, '23'], + [1572098175.934, '24'], + [1572098190.937, '25'], + [1572098205.934, '26'], + [1572098220.933, '27'], + [1572098235.934, '28'], + ], + }, + { + metric: { + __name__: 'promhttp_metric_handler_requests_total', + code: '500', + instance: 'localhost:9090', + job: 'prometheus', + }, + values: [ + [1572097950.93, '0'], + [1572097965.931, '0'], + [1572097980.929, '0'], + [1572097995.931, '0'], + [1572098010.932, '0'], + [1572098025.933, '0'], + [1572098040.93, '0'], + [1572098055.93, '0'], + [1572098070.93, '0'], + [1572098085.936, '0'], + [1572098100.936, '0'], + [1572098115.933, '0'], + [1572098130.932, '0'], + [1572098145.932, '0'], + [1572098160.933, '0'], + [1572098175.934, '0'], + [1572098190.937, '0'], + [1572098205.934, '0'], + [1572098220.933, '0'], + [1572098235.934, '0'], + ], + }, + { + metric: { + __name__: 'promhttp_metric_handler_requests_total', + code: '503', + instance: 'localhost:9090', + job: 'prometheus', + }, + values: [ + [1572097950.93, '0'], + [1572097965.931, '0'], + [1572097980.929, '0'], + [1572097995.931, '0'], + [1572098010.932, '0'], + [1572098025.933, '0'], + [1572098040.93, '0'], + [1572098055.93, '0'], + [1572098070.93, '0'], + [1572098085.936, '0'], + [1572098100.936, '0'], + [1572098115.933, '0'], + [1572098130.932, '0'], + [1572098145.932, '0'], + [1572098160.933, '0'], + [1572098175.934, '0'], + [1572098190.937, '0'], + [1572098205.934, '0'], + [1572098220.933, '0'], + [1572098235.934, '0'], + ], + }, + ], + }, + }; + const dataTable = shallow(); + it('renders rows', () => { + const table = dataTable.find(Table); + const rows = table.find('tr'); + expect(table.find('tr')).toHaveLength(3); + const row = rows.at(0); + expect(row.text()).toEqual(`1 @1572097950.93 +1 @1572097965.931 +1 @1572097980.929 +1 @1572097995.931 +1 @1572098010.932 +1 @1572098025.933 +1 @1572098040.93 +1 @1572098055.93 +1 @1572098070.93 +1 @1572098085.936 +1 @1572098100.936 +1 @1572098115.933 +1 @1572098130.932 +1 @1572098145.932 +1 @1572098160.933 +1 @1572098175.934 +1 @1572098190.937 +1 @1572098205.934 +1 @1572098220.933 +1 @1572098235.934`); + }); + }); + + describe('when resultType is a scalar', () => { + const dataTableProps: QueryResult = { + data: { + resultType: 'scalar', + result: [1572098246.599, '5'], + }, + }; + const dataTable = shallow(); + it('renders a scalar row', () => { + const table = dataTable.find(Table); + const rows = table.find('tr'); + expect(rows.text()).toEqual('scalar5'); + }); + }); + + describe('when resultType is a string', () => { + const dataTableProps: QueryResult = { + data: { + resultType: 'string', + result: 'string', + }, + }; + const dataTable = shallow(); + it('renders a string row', () => { + const table = dataTable.find(Table); + const rows = table.find('tr'); + expect(rows.text()).toEqual('scalart'); + }); + }); +}); diff --git a/web/ui/react-app/src/DataTable.tsx b/web/ui/react-app/src/DataTable.tsx index b6012bffc9..9f1a52ceb3 100644 --- a/web/ui/react-app/src/DataTable.tsx +++ b/web/ui/react-app/src/DataTable.tsx @@ -100,7 +100,7 @@ class DataTable extends PureComponent { break; case 'scalar': rows.push( - + scalar {data.result[1]} @@ -108,7 +108,7 @@ class DataTable extends PureComponent { break; case 'string': rows.push( - + scalar {data.result[1]} diff --git a/web/ui/react-app/src/ExpressionInput.test.tsx b/web/ui/react-app/src/ExpressionInput.test.tsx new file mode 100644 index 0000000000..9c985c7670 --- /dev/null +++ b/web/ui/react-app/src/ExpressionInput.test.tsx @@ -0,0 +1,145 @@ +import * as React from 'react'; +import { mount } from 'enzyme'; +import ExpressionInput from './ExpressionInput'; +import Downshift from 'downshift'; +import { Button, InputGroup, InputGroupAddon, Input } from 'reactstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons'; +import SanitizeHTML from './components/SanitizeHTML'; + +const getKeyEvent = (key: string): React.KeyboardEvent => + ({ + key, + nativeEvent: {}, + preventDefault: () => {}, + } as React.KeyboardEvent); + +describe('ExpressionInput', () => { + const metricNames = ['instance:node_cpu_utilisation:rate1m', 'node_cpu_guest_seconds_total', 'node_cpu_seconds_total']; + const expressionInputProps = { + value: 'node_cpu', + autocompleteSections: { + 'Query History': [], + 'Metric Names': metricNames, + }, + executeQuery: (): void => {}, + loading: false, + }; + const expressionInput = mount(); + + it('renders a downshift component', () => { + const downshift = expressionInput.find(Downshift); + expect(downshift.prop('inputValue')).toEqual('node_cpu'); + }); + + it('renders an InputGroup', () => { + const inputGroup = expressionInput.find(InputGroup); + expect(inputGroup.prop('className')).toEqual('expression-input'); + }); + + it('renders a search icon when it is not loading', () => { + const addon = expressionInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === 'prepend'); + const icon = addon.find(FontAwesomeIcon); + expect(icon.prop('icon')).toEqual(faSearch); + }); + + it('renders a loading icon when it is loading', () => { + const expressionInput = mount(); + const addon = expressionInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === 'prepend'); + const icon = addon.find(FontAwesomeIcon); + expect(icon.prop('icon')).toEqual(faSpinner); + expect(icon.prop('spin')).toBe(true); + }); + + it('renders an Input', () => { + const expressionInput = mount(); + const input = expressionInput.find(Input); + expect(input.prop('style')).toEqual({ height: 0 }); + expect(input.prop('autoFocus')).toEqual(true); + expect(input.prop('type')).toEqual('textarea'); + expect(input.prop('rows')).toEqual('1'); + expect(input.prop('placeholder')).toEqual('Expression (press Shift+Enter for newlines)'); + expect(expressionInput.state('value')).toEqual('node_cpu'); + }); + + describe('when autosuggest is closed', () => { + it('prevents Downshift default on Home, End, Arrows', () => { + const downshift = expressionInput.find(Downshift); + const input = downshift.find(Input); + downshift.setState({ isOpen: false }); + const onKeyDown = input.prop('onKeyDown'); + ['Home', 'End', 'ArrowUp', 'ArrowDown'].forEach(key => { + const event = getKeyEvent(key); + input.simulate('keydown', event); + const nativeEvent = event.nativeEvent as any; + expect(nativeEvent.preventDownshiftDefault).toBe(true); + }); + }); + + it('does not render an autosuggest', () => { + const downshift = expressionInput.find(Downshift); + downshift.setState({ isOpen: false }); + const ul = downshift.find('ul'); + expect(ul).toHaveLength(0); + }); + }); + + describe('when downshift is open', () => { + it('closes the menu on "Enter"', () => { + const downshift = expressionInput.find(Downshift); + const input = downshift.find(Input); + downshift.setState({ isOpen: true }); + const event = getKeyEvent('Enter'); + input.simulate('keydown', event); + expect(downshift.state('isOpen')).toBe(false); + }); + + it('noops on ArrowUp or ArrowDown', () => { + const downshift = expressionInput.find(Downshift); + const input = downshift.find(Input); + downshift.setState({ isOpen: true }); + ['ArrowUp', 'ArrowDown'].forEach(key => { + const event = getKeyEvent(key); + input.simulate('keydown', event); + const nativeEvent = event.nativeEvent as any; + expect(nativeEvent.preventDownshiftDefault).toBeUndefined(); + }); + }); + + it('does not render an autosuggest if there are no matches', () => { + const expressionInputProps = { + value: 'foo', + autocompleteSections: { + 'Query History': [], + 'Metric Names': [], + }, + executeQuery: (): void => {}, + loading: false, + }; + const expressionInput = mount(); + const downshift = expressionInput.find(Downshift); + downshift.setState({ isOpen: true }); + const ul = downshift.find('ul'); + expect(ul).toHaveLength(0); + }); + + it('renders an autosuggest if there are matches', () => { + const downshift = expressionInput.find(Downshift); + downshift.setState({ isOpen: true }); + const ul = downshift.find('ul'); + expect(ul.prop('className')).toEqual('card list-group'); + const items = ul.find(SanitizeHTML); + expect(items.map(item => item.text()).join(', ')).toEqual( + 'node_cpu_guest_seconds_total, node_cpu_seconds_total, instance:node_cpu_utilisation:rate1m' + ); + }); + }); + + it('renders an execute Button', () => { + const addon = expressionInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === 'append'); + const button = addon.find(Button); + expect(button.prop('className')).toEqual('execute-btn'); + expect(button.prop('color')).toEqual('primary'); + expect(button.text()).toEqual('Execute'); + }); +}); diff --git a/web/ui/react-app/src/Graph.test.tsx b/web/ui/react-app/src/Graph.test.tsx new file mode 100644 index 0000000000..9fcfff6e53 --- /dev/null +++ b/web/ui/react-app/src/Graph.test.tsx @@ -0,0 +1,105 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Graph from './Graph'; +import { Alert } from 'reactstrap'; +import ReactResizeDetector from 'react-resize-detector'; +import Legend from './Legend'; + +describe('Graph', () => { + [ + { + data: null, + color: 'light', + children: 'No data queried yet', + }, + { + data: { resultType: 'invalid' }, + color: 'danger', + children: `Query result is of wrong type '`, + }, + { + data: { + resultType: 'matrix', + result: [], + }, + color: 'secondary', + children: 'Empty query result', + }, + ].forEach(testCase => { + it(`renders an alert if data is "${testCase.data}"`, () => { + const props = { + data: testCase.data, + stacked: false, + queryParams: { + startTime: 1572100210000, + endTime: 1572100217898, + resolution: 10, + }, + }; + const graph = shallow(); + const alert = graph.find(Alert); + expect(alert.prop('color')).toEqual(testCase.color); + expect(alert.childAt(0).text()).toEqual(testCase.children); + }); + }); + + describe('data is returned', () => { + const props = { + queryParams: { + startTime: 1572128592, + endTime: 1572130692, + resolution: 28, + }, + stacked: false, + data: { + resultType: 'matrix', + result: [ + { + metric: { + code: '200', + handler: '/graph', + instance: 'localhost:9090', + job: 'prometheus', + }, + values: [ + [1572128592, '23'], + [1572128620, '2'], + [1572128648, '4'], + [1572128676, '1'], + [1572128704, '2'], + [1572128732, '12'], + [1572128760, '1'], + [1572128788, '0'], + [1572128816, '0'], + [1572128844, '2'], + [1572128872, '5'], + [1572130384, '6'], + [1572130412, '7'], + [1572130440, '19'], + [1572130468, '33'], + [1572130496, '14'], + [1572130524, '7'], + [1572130552, '6'], + [1572130580, '0'], + [1572130608, '0'], + [1572130636, '0'], + [1572130664, '0'], + [1572130692, '0'], + ], + }, + ], + }, + }; + it('renders a graph with props', () => { + const graph = shallow(); + const div = graph.find('div').filterWhere(elem => elem.prop('className') === 'graph'); + const resize = div.find(ReactResizeDetector); + const innerdiv = div.find('div').filterWhere(elem => elem.prop('className') === 'graph-chart'); + const legend = graph.find(Legend); + expect(resize.prop('handleWidth')).toBe(true); + expect(div).toHaveLength(1); + expect(innerdiv).toHaveLength(1); + expect(legend).toHaveLength(1); + }); + }); +}); diff --git a/web/ui/react-app/src/Graph.tsx b/web/ui/react-app/src/Graph.tsx index f7f6b49a7b..5be47cdcb0 100644 --- a/web/ui/react-app/src/Graph.tsx +++ b/web/ui/react-app/src/Graph.tsx @@ -4,6 +4,7 @@ import ReactResizeDetector from 'react-resize-detector'; import { Alert } from 'reactstrap'; import Legend from './Legend'; +import { escapeHTML } from './utils/html'; require('flot'); require('flot/source/jquery.flot.crosshair'); @@ -32,26 +33,11 @@ class Graph extends PureComponent { private id: number = getGraphID(); private chartRef = React.createRef(); - escapeHTML(str: string) { - const entityMap: { [key: string]: string } = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - }; - - return String(str).replace(/[&<>"'/]/g, function(s) { - return entityMap[s]; - }); - } - renderLabels(labels: { [key: string]: string }) { - const labelStrings: string[] = []; + let labelStrings: string[] = []; for (const label in labels) { if (label !== '__name__') { - labelStrings.push('' + label + ': ' + this.escapeHTML(labels[label])); + labelStrings.push('' + label + ': ' + escapeHTML(labels[label])); } } return '
' + labelStrings.join('
') + '
'; diff --git a/web/ui/react-app/src/GraphControls.test.tsx b/web/ui/react-app/src/GraphControls.test.tsx new file mode 100755 index 0000000000..ad0a66bafc --- /dev/null +++ b/web/ui/react-app/src/GraphControls.test.tsx @@ -0,0 +1,165 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import GraphControls from './GraphControls'; +import { Button, ButtonGroup, Form, InputGroup, InputGroupAddon, Input } from 'reactstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faPlus, faMinus, faChartArea, faChartLine } from '@fortawesome/free-solid-svg-icons'; +import TimeInput from './TimeInput'; + +const defaultGraphControlProps = { + range: 60 * 60 * 24, + endTime: 1572100217898, + resolution: 10, + stacked: false, + + onChangeRange: (): void => {}, + onChangeEndTime: (): void => {}, + onChangeResolution: (): void => {}, + onChangeStacking: (): void => {}, +}; + +describe('GraphControls', () => { + it('renders a form', () => { + const controls = shallow(); + const form = controls.find(Form); + expect(form).toHaveLength(1); + expect(form.prop('className')).toEqual('graph-controls'); + expect(form.prop('inline')).toBe(true); + }); + + it('renders an Input Group for range', () => { + const controls = shallow(); + const form = controls.find(InputGroup); + expect(form).toHaveLength(1); + expect(form.prop('className')).toEqual('range-input'); + expect(form.prop('size')).toBe('sm'); + }); + + it('renders a decrease/increase range buttons', () => { + [ + { + position: 'prepend', + title: 'Decrease range', + icon: faMinus, + }, + { + position: 'append', + title: 'Increase range', + icon: faPlus, + }, + ].forEach(testCase => { + const controls = shallow(); + const addon = controls.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === testCase.position); + const button = addon.find(Button); + const icon = button.find(FontAwesomeIcon); + expect(button.prop('title')).toEqual(testCase.title); + expect(icon).toHaveLength(1); + expect(icon.prop('icon')).toEqual(testCase.icon); + expect(icon.prop('fixedWidth')).toBe(true); + }); + }); + + it('renders an Input for range', () => { + const controls = shallow(); + const form = controls.find(InputGroup); + const input = form.find(Input); + expect(input).toHaveLength(1); + expect(input.prop('defaultValue')).toEqual('1d'); + expect(input.prop('innerRef')).toEqual({ current: null }); + }); + + it('renders a TimeInput with props', () => { + const controls = shallow(); + const timeInput = controls.find(TimeInput); + expect(timeInput).toHaveLength(1); + expect(timeInput.prop('time')).toEqual(1572100217898); + expect(timeInput.prop('range')).toEqual(86400); + expect(timeInput.prop('placeholder')).toEqual('End time'); + }); + + it('renders a TimeInput with a callback', () => { + const results: (number | null)[] = []; + const onChange = (endTime: number | null): void => { + results.push(endTime); + }; + const controls = shallow(); + const timeInput = controls.find(TimeInput); + const onChangeTime = timeInput.prop('onChangeTime'); + if (onChangeTime) { + onChangeTime(5); + expect(results).toHaveLength(1); + expect(results[0]).toEqual(5); + results.pop(); + } else { + fail('Expected onChangeTime to be defined but it was not'); + } + }); + + it('renders a resolution Input with props', () => { + const controls = shallow(); + const input = controls.find(Input).filterWhere(input => input.prop('className') === 'resolution-input'); + expect(input.prop('placeholder')).toEqual('Res. (s)'); + expect(input.prop('defaultValue')).toEqual('10'); + expect(input.prop('innerRef')).toEqual({ current: null }); + expect(input.prop('bsSize')).toEqual('sm'); + }); + + it('renders a button group', () => { + const controls = shallow(); + const group = controls.find(ButtonGroup); + expect(group.prop('className')).toEqual('stacked-input'); + expect(group.prop('size')).toEqual('sm'); + }); + + it('renders buttons inside the button group', () => { + [ + { + title: 'Show unstacked line graph', + icon: faChartLine, + active: true, + }, + { + title: 'Show stacked graph', + icon: faChartArea, + active: false, + }, + ].forEach(testCase => { + const controls = shallow(); + const group = controls.find(ButtonGroup); + const btn = group.find(Button).filterWhere(btn => btn.prop('title') === testCase.title); + expect(btn.prop('active')).toEqual(testCase.active); + const icon = btn.find(FontAwesomeIcon); + expect(icon.prop('icon')).toEqual(testCase.icon); + }); + }); + + it('renders buttons with callbacks', () => { + [ + { + title: 'Show unstacked line graph', + active: true, + }, + { + title: 'Show stacked graph', + active: false, + }, + ].forEach(testCase => { + const results: boolean[] = []; + const onChange = (stacked: boolean): void => { + results.push(stacked); + }; + const controls = shallow(); + const group = controls.find(ButtonGroup); + const btn = group.find(Button).filterWhere(btn => btn.prop('title') === testCase.title); + const onClick = btn.prop('onClick'); + if (onClick) { + onClick({} as React.MouseEvent); + expect(results).toHaveLength(1); + expect(results[0]).toBe(!testCase.active); + results.pop(); + } else { + fail('Expected onClick to be defined but it was not'); + } + }); + }); +}); diff --git a/web/ui/react-app/src/Legend.test.tsx b/web/ui/react-app/src/Legend.test.tsx new file mode 100755 index 0000000000..f61dc4d1cd --- /dev/null +++ b/web/ui/react-app/src/Legend.test.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Legend from './Legend'; +import SeriesName from './SeriesName'; + +describe('Legend', () => { + describe('regardless of series', () => { + it('renders a table', () => { + const legend = shallow(); + expect(legend.type()).toEqual('table'); + expect(legend.prop('className')).toEqual('graph-legend'); + const tbody = legend.children(); + expect(tbody.type()).toEqual('tbody'); + }); + }); + describe('when series is empty', () => { + it('renders props as empty legend table', () => { + const legend = shallow(); + const tbody = legend.children(); + expect(tbody.children()).toHaveLength(0); + }); + }); + + describe('when series has one element', () => { + const legendProps = { + series: [ + { + index: 1, + color: 'red', + labels: { + __name__: 'metric_name', + label1: 'value_1', + labeln: 'value_n', + }, + }, + ], + }; + it('renders a row of the one series', () => { + const legend = shallow(); + const tbody = legend.children(); + expect(tbody.children()).toHaveLength(1); + const row = tbody.find('tr'); + expect(row.prop('className')).toEqual('legend-item'); + }); + it('renders a legend swatch', () => { + const legend = shallow(); + const tbody = legend.children(); + const row = tbody.find('tr'); + const swatch = row.childAt(0); + expect(swatch.type()).toEqual('td'); + expect(swatch.children().prop('className')).toEqual('legend-swatch'); + expect(swatch.children().prop('style')).toEqual({ + backgroundColor: 'red', + }); + }); + it('renders a series name', () => { + const legend = shallow(); + const tbody = legend.children(); + const row = tbody.find('tr'); + const series = row.childAt(1); + expect(series.type()).toEqual('td'); + const seriesName = series.find(SeriesName); + expect(seriesName).toHaveLength(1); + expect(seriesName.prop('labels')).toEqual(legendProps.series[0].labels); + expect(seriesName.prop('format')).toBe(true); + }); + }); + + describe('when series has _n_ elements', () => { + const range = Array.from(Array(20).keys()); + const legendProps = { + series: range.map(i => ({ + index: i, + color: 'red', + labels: { + __name__: `metric_name_${i}`, + label1: 'value_1', + labeln: 'value_n', + }, + })), + }; + it('renders _n_ rows', () => { + const legend = shallow(); + const tbody = legend.children(); + expect(tbody.children()).toHaveLength(20); + const rows = tbody.find('tr'); + rows.forEach(row => { + expect(row.prop('className')).toEqual('legend-item'); + expect(row.find(SeriesName)).toHaveLength(1); + }); + }); + }); +}); diff --git a/web/ui/react-app/src/MetricFormat.test.ts b/web/ui/react-app/src/MetricFormat.test.ts new file mode 100644 index 0000000000..d98e44a8f6 --- /dev/null +++ b/web/ui/react-app/src/MetricFormat.test.ts @@ -0,0 +1,25 @@ +import metricToSeriesName from './MetricFormat'; + +describe('metricToSeriesName', () => { + it('returns "{}" if labels is empty', () => { + const labels = {}; + expect(metricToSeriesName(labels)).toEqual('{}'); + }); + it('returns "metric_name{}" if labels only contains __name__', () => { + const labels = { __name__: 'metric_name' }; + expect(metricToSeriesName(labels)).toEqual('metric_name{}'); + }); + it('returns "{label1=value_1, ..., labeln=value_n} if there are many labels and no name', () => { + const labels = { label1: 'value_1', label2: 'value_2', label3: 'value_3' }; + expect(metricToSeriesName(labels)).toEqual('{label1="value_1", label2="value_2", label3="value_3"}'); + }); + it('returns "metric_name{label1=value_1, ... ,labeln=value_n}" if there are many labels and a name', () => { + const labels = { + __name__: 'metric_name', + label1: 'value_1', + label2: 'value_2', + label3: 'value_3', + }; + expect(metricToSeriesName(labels)).toEqual('metric_name{label1="value_1", label2="value_2", label3="value_3"}'); + }); +}); diff --git a/web/ui/react-app/src/MetricFomat.ts b/web/ui/react-app/src/MetricFormat.ts similarity index 100% rename from web/ui/react-app/src/MetricFomat.ts rename to web/ui/react-app/src/MetricFormat.ts diff --git a/web/ui/react-app/src/Panel.test.tsx b/web/ui/react-app/src/Panel.test.tsx new file mode 100644 index 0000000000..9dbcde1efa --- /dev/null +++ b/web/ui/react-app/src/Panel.test.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; +import { mount, shallow } from 'enzyme'; +import Panel, { PanelOptions, PanelType } from './Panel'; +import ExpressionInput from './ExpressionInput'; +import GraphControls from './GraphControls'; +import Graph from './Graph'; +import { NavLink, TabPane } from 'reactstrap'; +import TimeInput from './TimeInput'; +import DataTable from './DataTable'; + +describe('Panel', () => { + const props = { + options: { + expr: 'prometheus_engine', + type: PanelType.Table, + range: 10, + endTime: 1572100217898, + resolution: 28, + stacked: false, + }, + onOptionsChanged: (): void => {}, + pastQueries: [], + metricNames: [ + 'prometheus_engine_queries', + 'prometheus_engine_queries_concurrent_max', + 'prometheus_engine_query_duration_seconds', + ], + removePanel: (): void => {}, + onExecuteQuery: (): void => {}, + }; + const panel = shallow(); + + it('renders an ExpressionInput', () => { + const input = panel.find(ExpressionInput); + expect(input.prop('value')).toEqual('prometheus_engine'); + expect(input.prop('autocompleteSections')).toEqual({ + 'Metric Names': [ + 'prometheus_engine_queries', + 'prometheus_engine_queries_concurrent_max', + 'prometheus_engine_query_duration_seconds', + ], + 'Query History': [], + }); + }); + + it('renders NavLinks', () => { + const results: PanelOptions[] = []; + const onOptionsChanged = (opts: PanelOptions): void => { + results.push(opts); + }; + const panel = shallow(); + const links = panel.find(NavLink); + [{ panelType: 'Table', active: true }, { panelType: 'Graph', active: false }].forEach( + (tc: { panelType: string; active: boolean }, i: number) => { + const link = links.at(i); + const className = tc.active ? 'active' : ''; + expect(link.prop('className')).toEqual(className); + link.simulate('click'); + expect(results).toHaveLength(1); + expect(results[0].type).toEqual(tc.panelType.toLowerCase()); + results.pop(); + } + ); + }); + + it('renders a TabPane with a TimeInput and a DataTable when in table mode', () => { + const tab = panel.find(TabPane).filterWhere(tab => tab.prop('tabId') === 'table'); + const timeInput = tab.find(TimeInput); + expect(timeInput.prop('time')).toEqual(props.options.endTime); + expect(timeInput.prop('range')).toEqual(props.options.range); + expect(timeInput.prop('placeholder')).toEqual('Evaluation time'); + expect(tab.find(DataTable)).toHaveLength(1); + }); + + it('renders a TabPane with a Graph and GraphControls when in graph mode', () => { + const options = { + expr: 'prometheus_engine', + type: PanelType.Graph, + range: 10, + endTime: 1572100217898, + resolution: 28, + stacked: false, + }; + const graphPanel = mount(); + const controls = graphPanel.find(GraphControls); + const graph = graphPanel.find(Graph); + expect(controls.prop('endTime')).toEqual(props.options.endTime); + expect(controls.prop('range')).toEqual(props.options.range); + expect(controls.prop('resolution')).toEqual(props.options.resolution); + expect(controls.prop('stacked')).toEqual(props.options.stacked); + expect(graph.prop('stacked')).toEqual(props.options.stacked); + }); +}); diff --git a/web/ui/react-app/src/PanelList.test.tsx b/web/ui/react-app/src/PanelList.test.tsx new file mode 100755 index 0000000000..1d7b230b30 --- /dev/null +++ b/web/ui/react-app/src/PanelList.test.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { mount, shallow } from 'enzyme'; +import PanelList from './pages/PanelList'; +import Checkbox from './Checkbox'; +import { Alert, Button } from 'reactstrap'; +import Panel from './Panel'; + +describe('PanelList', () => { + it('renders a query history checkbox', () => { + const panelList = shallow(); + const checkbox = panelList.find(Checkbox); + expect(checkbox.prop('id')).toEqual('query-history-checkbox'); + expect(checkbox.prop('wrapperStyles')).toEqual({ + margin: '0 0 0 15px', + alignSelf: 'center', + }); + expect(checkbox.prop('defaultChecked')).toBe(false); + expect(checkbox.children().text()).toBe('Enable query history'); + }); + + it('renders an alert when no data is queried yet', () => { + const panelList = mount(); + const alert = panelList.find(Alert); + expect(alert.prop('color')).toEqual('light'); + expect(alert.children().text()).toEqual('No data queried yet'); + }); + + it('renders panels', () => { + const panelList = shallow(); + const panels = panelList.find(Panel); + expect(panels.length).toBeGreaterThan(0); + }); + + it('renders a button to add a panel', () => { + const panelList = shallow(); + const btn = panelList.find(Button).filterWhere(btn => btn.prop('className') === 'add-panel-btn'); + expect(btn.prop('color')).toEqual('primary'); + expect(btn.children().text()).toEqual('Add Panel'); + }); +}); diff --git a/web/ui/react-app/src/QueryStatsView.test.tsx b/web/ui/react-app/src/QueryStatsView.test.tsx new file mode 100755 index 0000000000..e04c914e1a --- /dev/null +++ b/web/ui/react-app/src/QueryStatsView.test.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import QueryStatsView from './QueryStatsView'; + +describe('QueryStatsView', () => { + it('renders props as query stats', () => { + const queryStatsProps = { + loadTime: 100, + resolution: 5, + resultSeries: 10000, + }; + const queryStatsView = shallow(); + expect(queryStatsView.prop('className')).toEqual('query-stats'); + expect(queryStatsView.children().prop('className')).toEqual('float-right'); + expect(queryStatsView.children().text()).toEqual('Load time: 100ms   Resolution: 5s   Result series: 10000'); + }); +}); diff --git a/web/ui/react-app/src/SeriesName.test.tsx b/web/ui/react-app/src/SeriesName.test.tsx new file mode 100755 index 0000000000..4eaf68105c --- /dev/null +++ b/web/ui/react-app/src/SeriesName.test.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import SeriesName from './SeriesName'; + +describe('SeriesName', () => { + describe('with labels=null', () => { + const seriesNameProps = { + labels: null, + format: false, + }; + const seriesName = shallow(); + it('renders the string "scalar"', () => { + expect(seriesName.text()).toEqual('scalar'); + }); + }); + + describe('with labels defined and format false', () => { + const seriesNameProps = { + labels: { + __name__: 'metric_name', + label1: 'value_1', + label2: 'value_2', + label3: 'value_3', + }, + format: false, + }; + const seriesName = shallow(); + it('renders the series name as a string', () => { + expect(seriesName.text()).toEqual('metric_name{label1="value_1", label2="value_2", label3="value_3"}'); + }); + }); + + describe('with labels defined and format true', () => { + const seriesNameProps = { + labels: { + __name__: 'metric_name', + label1: 'value_1', + label2: 'value_2', + label3: 'value_3', + }, + format: true, + }; + const seriesName = shallow(); + it('renders the series name as a series of spans', () => { + expect(seriesName.children()).toHaveLength(6); + const testCases = [ + { name: 'metric_name', className: 'legend-metric-name' }, + { name: '{', className: 'legend-label-brace' }, + { name: 'label1', value: 'value_1', className: 'legend-label-name' }, + { name: 'label2', value: 'value_2', className: 'legend-label-name' }, + { name: 'label3', value: 'value_3', className: 'legend-label-name' }, + { name: '}', className: 'legend-label-brace' }, + ]; + testCases.forEach((tc, i) => { + const child = seriesName.childAt(i); + const text = child + .children() + .map(ch => ch.text()) + .join(''); + switch (child.children().length) { + case 1: + expect(text).toEqual(tc.name); + expect(child.prop('className')).toEqual(tc.className); + break; + case 3: + expect(text).toEqual(`${tc.name}="${tc.value}"`); + expect(child.childAt(0).prop('className')).toEqual('legend-label-name'); + expect(child.childAt(2).prop('className')).toEqual('legend-label-value'); + break; + case 4: + expect(text).toEqual(`, ${tc.name}="${tc.value}"`); + expect(child.childAt(1).prop('className')).toEqual('legend-label-name'); + expect(child.childAt(3).prop('className')).toEqual('legend-label-value'); + break; + default: + fail('incorrect number of children: ' + child.children().length); + } + }); + }); + }); +}); diff --git a/web/ui/react-app/src/SeriesName.tsx b/web/ui/react-app/src/SeriesName.tsx index 1da4882570..edbe1d97d9 100644 --- a/web/ui/react-app/src/SeriesName.tsx +++ b/web/ui/react-app/src/SeriesName.tsx @@ -1,4 +1,5 @@ import React, { PureComponent } from 'react'; +import metricToSeriesName from './MetricFormat'; interface SeriesNameProps { labels: { [key: string]: string } | null; @@ -40,16 +41,7 @@ class SeriesName extends PureComponent { renderPlain() { const labels = this.props.labels!; - - let tsName = (labels.__name__ || '') + '{'; - const labelStrings: string[] = []; - for (const label in labels) { - if (label !== '__name__') { - labelStrings.push(label + '="' + labels[label] + '"'); - } - } - tsName += labelStrings.join(', ') + '}'; - return tsName; + return metricToSeriesName(labels); } render() { diff --git a/web/ui/react-app/src/TimeInput.test.tsx b/web/ui/react-app/src/TimeInput.test.tsx new file mode 100644 index 0000000000..ad9043499a --- /dev/null +++ b/web/ui/react-app/src/TimeInput.test.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import TimeInput from './TimeInput'; +import { Button, InputGroup, InputGroupAddon, Input } from 'reactstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faChevronLeft, faChevronRight, faTimes } from '@fortawesome/free-solid-svg-icons'; + +describe('TimeInput', () => { + const timeInputProps = { + time: 1572102237932, + range: 60 * 60 * 7, + placeholder: 'time input', + onChangeTime: (): void => {}, + }; + const timeInput = shallow(); + it('renders the string "scalar"', () => { + const inputGroup = timeInput.find(InputGroup); + expect(inputGroup.prop('className')).toEqual('time-input'); + expect(inputGroup.prop('size')).toEqual('sm'); + }); + + it('renders buttons to adjust time', () => { + [ + { + position: 'prepend', + title: 'Decrease time', + icon: faChevronLeft, + }, + { + position: 'append', + title: 'Clear time', + icon: faTimes, + }, + { + position: 'append', + title: 'Increase time', + icon: faChevronRight, + }, + ].forEach(button => { + const onChangeTime = sinon.spy(); + const timeInput = shallow(); + const addon = timeInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === button.position); + const btn = addon.find(Button).filterWhere(btn => btn.prop('title') === button.title); + const icon = btn.find(FontAwesomeIcon); + expect(icon.prop('icon')).toEqual(button.icon); + expect(icon.prop('fixedWidth')).toBe(true); + btn.simulate('click'); + expect(onChangeTime.calledOnce).toBe(true); + }); + }); + + it('renders an Input', () => { + const input = timeInput.find(Input); + expect(input.prop('placeholder')).toEqual(timeInputProps.placeholder); + expect(input.prop('innerRef')).toEqual({ current: null }); + }); +}); diff --git a/web/ui/react-app/src/components/SanitizeHTML/SanitizeHTML.test.tsx b/web/ui/react-app/src/components/SanitizeHTML/SanitizeHTML.test.tsx new file mode 100644 index 0000000000..2dd47c95f9 --- /dev/null +++ b/web/ui/react-app/src/components/SanitizeHTML/SanitizeHTML.test.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import SanitizeHTML from '.'; + +describe('SanitizeHTML', () => { + it(`renders allowed html`, () => { + const props = { + allowedTags: ['strong'], + }; + const html = shallow({'text'}); + const elem = html.find('div'); + expect(elem).toHaveLength(1); + expect(elem.html()).toEqual(`
text
`); + }); + + it('does not render disallowed tags', () => { + const props = { + tag: 'span' as keyof JSX.IntrinsicElements, + allowedTags: ['strong'], + }; + const html = shallow({'link'}); + const elem = html.find('span'); + expect(elem.html()).toEqual('link'); + }); +}); diff --git a/web/ui/react-app/src/components/SanitizeHTML/test.js b/web/ui/react-app/src/components/SanitizeHTML/test.js deleted file mode 100644 index e8f8c62355..0000000000 --- a/web/ui/react-app/src/components/SanitizeHTML/test.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * SanitizeHTML tests - */ -import React from 'react'; -import ReactDOM from 'react-dom'; -import SanitizeHTML from '../SanitizeHTML'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); -}); diff --git a/web/ui/react-app/src/setupTests.ts b/web/ui/react-app/src/setupTests.ts new file mode 100644 index 0000000000..08699661fe --- /dev/null +++ b/web/ui/react-app/src/setupTests.ts @@ -0,0 +1,5 @@ +import { configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import './globals'; + +configure({ adapter: new Adapter() }); diff --git a/web/ui/react-app/src/utils/func.ts b/web/ui/react-app/src/utils/func.ts deleted file mode 100644 index 3b0412fb49..0000000000 --- a/web/ui/react-app/src/utils/func.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const uuidGen = () => - '_' + - Math.random() - .toString(36) - .substr(2, 9); diff --git a/web/ui/react-app/src/utils/html.test.ts b/web/ui/react-app/src/utils/html.test.ts new file mode 100644 index 0000000000..2db5e83237 --- /dev/null +++ b/web/ui/react-app/src/utils/html.test.ts @@ -0,0 +1,9 @@ +import { escapeHTML } from './html'; + +describe('escapeHTML', (): void => { + it('escapes html sequences', () => { + expect(escapeHTML(`'example'&"another/example"`)).toEqual( + '<strong>'example'&"another/example"</strong>' + ); + }); +}); diff --git a/web/ui/react-app/src/utils/html.ts b/web/ui/react-app/src/utils/html.ts new file mode 100644 index 0000000000..e4e101699e --- /dev/null +++ b/web/ui/react-app/src/utils/html.ts @@ -0,0 +1,14 @@ +export const escapeHTML = (str: string): string => { + const entityMap: { [key: string]: string } = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + }; + + return String(str).replace(/[&<>"'/]/g, function(s) { + return entityMap[s]; + }); +}; diff --git a/web/ui/react-app/src/utils/timeFormat.test.ts b/web/ui/react-app/src/utils/timeFormat.test.ts new file mode 100644 index 0000000000..a5cc398d2b --- /dev/null +++ b/web/ui/react-app/src/utils/timeFormat.test.ts @@ -0,0 +1,37 @@ +import { formatTime, parseTime, formatRange, parseRange } from './timeFormat'; + +describe('formatTime', () => { + it('returns a time string representing the time in seconds', () => { + expect(formatTime(1572049380000)).toEqual('2019-10-26 00:23'); + expect(formatTime(0)).toEqual('1970-01-01 00:00'); + }); +}); + +describe('parseTime', () => { + it('returns a time string representing the time in seconds', () => { + expect(parseTime('2019-10-26 00:23')).toEqual(1572049380000); + expect(parseTime('1970-01-01 00:00')).toEqual(0); + }); +}); + +describe('formatRange', () => { + it('returns a time string representing the time in seconds in one unit', () => { + expect(formatRange(60 * 60 * 24 * 365)).toEqual('1y'); + expect(formatRange(60 * 60 * 24 * 7)).toEqual('1w'); + expect(formatRange(2 * 60 * 60 * 24)).toEqual('2d'); + expect(formatRange(60 * 60)).toEqual('1h'); + expect(formatRange(7 * 60)).toEqual('7m'); + expect(formatRange(63)).toEqual('63s'); + }); +}); + +describe('parseRange', () => { + it('returns a time string representing the time in seconds in one unit', () => { + expect(parseRange('1y')).toEqual(60 * 60 * 24 * 365); + expect(parseRange('1w')).toEqual(60 * 60 * 24 * 7); + expect(parseRange('2d')).toEqual(2 * 60 * 60 * 24); + expect(parseRange('1h')).toEqual(60 * 60); + expect(parseRange('7m')).toEqual(7 * 60); + expect(parseRange('63s')).toEqual(63); + }); +}); diff --git a/web/ui/react-app/src/utils/urlParams.test.ts b/web/ui/react-app/src/utils/urlParams.test.ts new file mode 100644 index 0000000000..d117796614 --- /dev/null +++ b/web/ui/react-app/src/utils/urlParams.test.ts @@ -0,0 +1,47 @@ +import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString } from './urlParams'; +import { PanelType } from '../Panel'; + +const panels = [ + { + key: '0', + options: { + endTime: 1572046620000, + expr: 'rate(node_cpu_seconds_total{mode="system"}[1m])', + range: 3600, + resolution: null, + stacked: false, + type: PanelType.Graph, + }, + }, + { + key: '1', + options: { + endTime: null, + expr: 'node_filesystem_avail_bytes', + range: 3600, + resolution: null, + stacked: false, + type: PanelType.Table, + }, + }, +]; +const query = + '?g0.expr=rate(node_cpu_seconds_total%7Bmode%3D%22system%22%7D%5B1m%5D)&g0.tab=0&g0.stacked=0&g0.range_input=1h&g0.end_input=2019-10-25%2023%3A37&g0.moment_input=2019-10-25%2023%3A37&g1.expr=node_filesystem_avail_bytes&g1.tab=1&g1.stacked=0&g1.range_input=1h'; + +describe('decodePanelOptionsFromQueryString', () => { + it('returns [] when query is empty', () => { + expect(decodePanelOptionsFromQueryString('')).toEqual([]); + }); + it('returns and array of parsed params when query string is non-empty', () => { + expect(decodePanelOptionsFromQueryString(query)).toEqual(panels); + }); +}); + +describe('encodePanelOptionsToQueryString', () => { + it('returns ? when panels is empty', () => { + expect(encodePanelOptionsToQueryString([])).toEqual('?'); + }); + it('returns an encoded query string otherwise', () => { + expect(encodePanelOptionsToQueryString(panels)).toEqual(query); + }); +}); diff --git a/web/ui/react-app/yarn.lock b/web/ui/react-app/yarn.lock index d8758999ec..c8a025cd67 100644 --- a/web/ui/react-app/yarn.lock +++ b/web/ui/react-app/yarn.lock @@ -1150,6 +1150,35 @@ react-lifecycles-compat "^3.0.4" warning "^3.0.0" +"@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.6.0.tgz#ec7670432ae9c8eb710400d112c201a362d83393" + integrity sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg== + dependencies: + type-detect "4.0.8" + +"@sinonjs/formatio@^3.2.1": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.2.tgz#771c60dfa75ea7f2d68e3b94c7e888a78781372c" + integrity sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ== + dependencies: + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^3.1.0" + +"@sinonjs/samsam@^3.1.0", "@sinonjs/samsam@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a" + integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ== + dependencies: + "@sinonjs/commons" "^1.3.0" + array-from "^2.1.1" + lodash "^4.17.15" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== + "@svgr/babel-plugin-add-jsx-attribute@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz#dadcb6218503532d6884b210e7f3c502caaa44b1" @@ -1286,6 +1315,13 @@ dependencies: "@babel/types" "^7.3.0" +"@types/cheerio@*": + version "0.22.13" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.13.tgz#5eecda091a24514185dcba99eda77e62bf6523e6" + integrity sha512-OZd7dCUOUkiTorf97vJKwZnSja/DmHfuBAroe1kREZZTCf/tlFecwHhsOos3uVHxeKGZDwzolIrCUApClkdLuA== + dependencies: + "@types/node" "*" + "@types/domhandler@*": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/domhandler/-/domhandler-2.4.1.tgz#7b3b347f7762180fbcb1ece1ce3dd0ebbb8c64cf" @@ -1298,6 +1334,21 @@ dependencies: "@types/domhandler" "*" +"@types/enzyme-adapter-react-16@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.5.tgz#1bf30a166f49be69eeda4b81e3f24113c8b4e9d5" + integrity sha512-K7HLFTkBDN5RyRmU90JuYt8OWEY2iKUn43SDWEoBOXd/PowUWjLZ3Q6qMBiQuZeFYK/TOstaZxsnI0fXoAfLpg== + dependencies: + "@types/enzyme" "*" + +"@types/enzyme@*", "@types/enzyme@^3.10.3": + version "3.10.3" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.3.tgz#02b6c5ac7d0472005944a652e79045e2f6c66804" + integrity sha512-f/Kcb84sZOSZiBPCkr4He9/cpuSLcKRyQaEE20Q30Prx0Dn6wcyMAWI0yofL6yvd9Ht9G7EVkQeRqK0n5w8ILw== + dependencies: + "@types/cheerio" "*" + "@types/react" "*" + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -1349,10 +1400,10 @@ resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89" integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA== -"@types/jest@^24.0.4": - version "24.0.19" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.19.tgz#f7036058d2a5844fe922609187c0ad8be430aff5" - integrity sha512-YYiqfSjocv7lk5H/T+v5MjATYjaTMsUkbDnjGqSMoO88jWdtJXJV4ST/7DKZcoMHMBvB2SeSfyOzZfkxXHR5xg== +"@types/jest@^24.0.20": + version "24.0.20" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.20.tgz#729d5fe8684e7fb06368d3bd557ac6d91289d861" + integrity sha512-M8ebEkOpykGdLoRrmew7UowTZ1DANeeP0HiSIChl/4DGgmnSC1ntitNtkyNSXjMTsZvXuaxJrxjImEnRWNPsPw== dependencies: "@types/jest-diff" "*" @@ -1442,6 +1493,11 @@ dependencies: "@types/htmlparser2" "*" +"@types/sinon@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.0.tgz#f5a10c27175465a0b001b68d8b9f761582967cc6" + integrity sha512-NyzhuSBy97B/zE58cDw4NyGvByQbAHNP9069KVSgnXt/sc0T6MFRh0InKAeBVHJWdSXG1S3+PxgVIgKo9mTHbw== + "@types/sizzle@*": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" @@ -1769,6 +1825,22 @@ adjust-sourcemap-loader@2.0.0: object-path "0.11.4" regex-parser "2.2.10" +airbnb-prop-types@^2.15.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz#5287820043af1eb469f5b0af0d6f70da6c52aaef" + integrity sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA== + dependencies: + array.prototype.find "^2.1.0" + function.prototype.name "^1.1.1" + has "^1.0.3" + is-regex "^1.0.4" + object-is "^1.0.1" + object.assign "^4.1.0" + object.entries "^1.1.0" + prop-types "^15.7.2" + prop-types-exact "^1.2.0" + react-is "^16.9.0" + ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -1912,6 +1984,11 @@ array-equal@^1.0.0: resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= +array-filter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" + integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -1922,6 +1999,11 @@ array-flatten@^2.1.0: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== +array-from@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" + integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= + array-includes@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" @@ -1947,6 +2029,23 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= +array.prototype.find@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.0.tgz#630f2eaf70a39e608ac3573e45cf8ccd0ede9ad7" + integrity sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.13.0" + +array.prototype.flat@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.2.tgz#8f3c71d245ba349b6b64b4078f76f5576f1fd723" + integrity sha512-VXjh7lAL4KXKF2hY4FnEW9eRW6IhdvFW1sN/JwLbmECbCgACCnBHNyP3lFiYuttr0jxRN9Bsc5+G27dMseSWqQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.15.0" + function-bind "^1.1.1" + arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -2629,6 +2728,18 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +cheerio@^1.0.0-rc.2: + version "1.0.0-rc.3" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" + integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA== + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.1" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash "^4.15.0" + parse5 "^3.0.1" + chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.4: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -2837,7 +2948,7 @@ commander@2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== -commander@^2.11.0, commander@^2.20.0: +commander@^2.11.0, commander@^2.19.0, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -3169,7 +3280,7 @@ css-select-base-adapter@^0.1.1: resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== -css-select@^1.1.0: +css-select@^1.1.0, css-select@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= @@ -3548,6 +3659,11 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== +diff@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -3565,6 +3681,11 @@ dir-glob@2.0.0: arrify "^1.0.1" path-type "^3.0.0" +discontinuous-range@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" + integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo= + dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -3629,12 +3750,20 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" +dom-serializer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" + domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@1, domelementtype@^1.3.1: +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== @@ -3786,7 +3915,7 @@ enhanced-resolve@^4.1.0: memory-fs "^0.5.0" tapable "^1.0.0" -entities@^1.1.1: +entities@^1.1.1, entities@~1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== @@ -3796,6 +3925,68 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== +enzyme-adapter-react-16@^1.15.1: + version "1.15.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.1.tgz#8ad55332be7091dc53a25d7d38b3485fc2ba50d5" + integrity sha512-yMPxrP3vjJP+4wL/qqfkT6JAIctcwKF+zXO6utlGPgUJT2l4tzrdjMDWGd/Pp1BjHBcljhN24OzNEGRteibJhA== + dependencies: + enzyme-adapter-utils "^1.12.1" + enzyme-shallow-equal "^1.0.0" + has "^1.0.3" + object.assign "^4.1.0" + object.values "^1.1.0" + prop-types "^15.7.2" + react-is "^16.10.2" + react-test-renderer "^16.0.0-0" + semver "^5.7.0" + +enzyme-adapter-utils@^1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.1.tgz#e828e0d038e2b1efa4b9619ce896226f85c9dd88" + integrity sha512-KWiHzSjZaLEoDCOxY8Z1RAbUResbqKN5bZvenPbfKtWorJFVETUw754ebkuCQ3JKm0adx1kF8JaiR+PHPiP47g== + dependencies: + airbnb-prop-types "^2.15.0" + function.prototype.name "^1.1.1" + object.assign "^4.1.0" + object.fromentries "^2.0.1" + prop-types "^15.7.2" + semver "^5.7.0" + +enzyme-shallow-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.0.tgz#d8e4603495e6ea279038eef05a4bf4887b55dc69" + integrity sha512-VUf+q5o1EIv2ZaloNQQtWCJM9gpeux6vudGVH6vLmfPXFLRuxl5+Aq3U260wof9nn0b0i+P5OEUXm1vnxkRpXQ== + dependencies: + has "^1.0.3" + object-is "^1.0.1" + +enzyme@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.10.0.tgz#7218e347c4a7746e133f8e964aada4a3523452f6" + integrity sha512-p2yy9Y7t/PFbPoTvrWde7JIYB2ZyGC+NgTNbVEGvZ5/EyoYSr9aG/2rSbVvyNvMHEhw9/dmGUJHWtfQIEiX9pg== + dependencies: + array.prototype.flat "^1.2.1" + cheerio "^1.0.0-rc.2" + function.prototype.name "^1.1.0" + has "^1.0.3" + html-element-map "^1.0.0" + is-boolean-object "^1.0.0" + is-callable "^1.1.4" + is-number-object "^1.0.3" + is-regex "^1.0.4" + is-string "^1.0.4" + is-subset "^0.1.1" + lodash.escape "^4.0.1" + lodash.isequal "^4.5.0" + object-inspect "^1.6.0" + object-is "^1.0.1" + object.assign "^4.1.0" + object.entries "^1.0.4" + object.values "^1.0.4" + raf "^3.4.0" + rst-selector-parser "^2.2.3" + string.prototype.trim "^1.1.2" + errno@^0.1.3, errno@~0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" @@ -3826,6 +4017,22 @@ es-abstract@^1.12.0, es-abstract@^1.15.0, es-abstract@^1.5.1, es-abstract@^1.7.0 string.prototype.trimleft "^2.1.0" string.prototype.trimright "^2.1.0" +es-abstract@^1.13.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d" + integrity sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg== + dependencies: + es-to-primitive "^1.2.0" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.0" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-inspect "^1.6.0" + object-keys "^1.1.1" + string.prototype.trimleft "^2.1.0" + string.prototype.trimright "^2.1.0" + es-to-primitive@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" @@ -4635,11 +4842,26 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function.prototype.name@^1.1.0, function.prototype.name@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.1.tgz#6d252350803085abc2ad423d4fe3be2f9cbda392" + integrity sha512-e1NzkiJuw6xqVH7YSdiW/qDHebcmMhPNe6w+4ZYYEg0VA+LaLzx37RimbPLuonHhYGFGPx1ME2nSi74JiaCr/Q== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + functions-have-names "^1.1.1" + is-callable "^1.1.4" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +functions-have-names@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.0.tgz#83da7583e4ea0c9ac5ff530f73394b033e0bf77d" + integrity sha512-zKXyzksTeaCSw5wIX79iCA40YAa6CJMJgNg9wdkU/ERBrIdPSimPICYiLp65lRbSBqtiHql/HZfS2DyI/AH6tQ== + fuzzy@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/fuzzy/-/fuzzy-0.1.3.tgz#4c76ec2ff0ac1a36a9dccf9a00df8623078d4ed8" @@ -4957,6 +5179,13 @@ html-comment-regex@^1.1.0: resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== +html-element-map@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.1.0.tgz#e5aab9a834caf883b421f8bd9eaedcaac887d63c" + integrity sha512-iqiG3dTZmy+uUaTmHarTL+3/A2VW9ox/9uasKEZC+R/wAtUrTcRlXPSaPqsnWPfIu8wqn09jQNwMRqzL54jSYA== + dependencies: + array-filter "^1.0.0" + html-encoding-sniffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" @@ -4994,7 +5223,7 @@ html-webpack-plugin@4.0.0-beta.5: tapable "^1.1.0" util.promisify "1.0.0" -htmlparser2@^3.10.0, htmlparser2@^3.3.0: +htmlparser2@^3.10.0, htmlparser2@^3.3.0, htmlparser2@^3.9.1: version "3.10.1" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== @@ -5351,6 +5580,11 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-boolean-object@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93" + integrity sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M= + is-buffer@^1.0.2, is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -5470,6 +5704,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-number-object@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799" + integrity sha1-8mWrian0RQNO9q/xWo8AsA9VF5k= + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -5550,6 +5789,16 @@ is-stream@^1.0.1, is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +is-string@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64" + integrity sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ= + +is-subset@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" + integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= + is-svg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" @@ -5579,6 +5828,11 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -6259,6 +6513,11 @@ jsx-ast-utils@^2.1.0, jsx-ast-utils@^2.2.1: array-includes "^3.0.3" object.assign "^4.1.0" +just-extend@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc" + integrity sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw== + killable@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -6416,11 +6675,26 @@ lodash.clonedeep@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.escape@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" + integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= + lodash.escaperegexp@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= +lodash.flattendeep@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" + integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + lodash.isfunction@^3.0.9: version "3.0.9" resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" @@ -6486,7 +6760,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.5: +"lodash@>=3.5 <5", lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.5: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -6496,6 +6770,11 @@ loglevel@^1.4.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.4.tgz#f408f4f006db8354d0577dcf6d33485b3cb90d56" integrity sha512-p0b6mOGKcGa+7nnmKbpzR6qloPbrgLcnio++E+14Vo/XffOGwZtRpUhr8dTH/x2oCMmEoIU0Zwm3ZauhvYD17g== +lolex@^4.1.0, lolex@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7" + integrity sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -6816,6 +7095,11 @@ moment-timezone@^0.5.11, moment-timezone@^0.5.23: resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== +moo@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" + integrity sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw== + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -6888,6 +7172,17 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +nearley@^2.7.10: + version "2.19.0" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.0.tgz#37717781d0fd0f2bfc95e233ebd75678ca4bda46" + integrity sha512-2v52FTw7RPqieZr3Gth1luAXZR7Je6q3KaDHY5bjl/paDUdMu35fZ8ICNgiYJRr3tf3NMvIQQR1r27AvEr9CRA== + dependencies: + commander "^2.19.0" + moo "^0.4.3" + railroad-diagrams "^1.0.0" + randexp "0.4.6" + semver "^5.4.1" + needle@^2.2.1: version "2.4.0" resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" @@ -6917,6 +7212,17 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +nise@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.2.tgz#b6d29af10e48b321b307e10e065199338eeb2652" + integrity sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA== + dependencies: + "@sinonjs/formatio" "^3.2.1" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + lolex "^4.1.0" + path-to-regexp "^1.7.0" + no-case@^2.2.0: version "2.3.2" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" @@ -7173,7 +7479,7 @@ object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" -object.entries@^1.1.0: +object.entries@^1.0.4, object.entries@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519" integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA== @@ -7183,7 +7489,7 @@ object.entries@^1.1.0: function-bind "^1.1.1" has "^1.0.3" -object.fromentries@^2.0.0: +object.fromentries@^2.0.0, object.fromentries@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.1.tgz#050f077855c7af8ae6649f45c80b16ee2d31e704" integrity sha512-PUQv8Hbg3j2QX0IQYv3iAGCbGcu4yY4KQ92/dhA4sFSixBmSmp13UpDLs6jGK8rBtbmhNNIK99LD2k293jpiGA== @@ -7208,7 +7514,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.0: +object.values@^1.0.4, object.values@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9" integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg== @@ -7465,6 +7771,13 @@ parse5@5.1.0: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== +parse5@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" + integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA== + dependencies: + "@types/node" "*" + parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -7522,6 +7835,13 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + integrity sha1-Wf3g9DW62suhA6hOnTvGTpa5k30= + dependencies: + isarray "0.0.1" + path-type@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" @@ -8402,6 +8722,15 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.3" +prop-types-exact@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" + integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== + dependencies: + has "^1.0.3" + object.assign "^4.1.0" + reflect.ownkeys "^0.2.0" + prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -8524,13 +8853,26 @@ raf-schd@^4.0.2: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== -raf@3.4.1: +raf@3.4.1, raf@^3.4.0: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== dependencies: performance-now "^2.1.0" +railroad-diagrams@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" + integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= + +randexp@0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" + integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== + dependencies: + discontinuous-range "1.0.0" + ret "~0.1.10" + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -8637,6 +8979,11 @@ react-error-overlay@^6.0.3: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.3.tgz#c378c4b0a21e88b2e159a3e62b2f531fd63bf60d" integrity sha512-bOUvMWFQVk5oz8Ded9Xb7WVdEi3QGLC8tH7HmYP0Fdp4Bn3qw0tRFmr5TW6mvahzvmrK4a6bqWGfCevBflP+Xw== +react-is@^16.10.2, react-is@^16.8.6: + version "16.11.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa" + integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw== + react-is@^16.8.1, react-is@^16.8.4, react-is@^16.9.0: version "16.10.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.10.2.tgz#984120fd4d16800e9a738208ab1fba422d23b5ab" @@ -8731,6 +9078,16 @@ react-scripts@^3.2.0: optionalDependencies: fsevents "2.0.7" +react-test-renderer@^16.0.0-0: + version "16.11.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.11.0.tgz#72574566496462c808ac449b0287a4c0a1a7d8f8" + integrity sha512-nh9gDl8R4ut+ZNNb2EeKO5VMvTKxwzurbSMuGBoKtjpjbg8JK/u3eVPVNi1h1Ue+eYK9oSzJjb+K3lzLxyA4ag== + dependencies: + object-assign "^4.1.1" + prop-types "^15.6.2" + react-is "^16.8.6" + scheduler "^0.17.0" + react-transition-group@^2.3.1: version "2.9.0" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" @@ -8851,6 +9208,11 @@ recursive-readdir@2.2.2: dependencies: minimatch "3.0.4" +reflect.ownkeys@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" + integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= + regenerate-unicode-properties@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" @@ -9135,6 +9497,14 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +rst-selector-parser@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" + integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE= + dependencies: + lodash.flattendeep "^4.4.0" + nearley "^2.7.10" + rsvp@^4.8.4: version "4.8.5" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" @@ -9245,6 +9615,14 @@ scheduler@^0.16.2: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.17.0.tgz#7c9c673e4ec781fac853927916d1c426b6f3ddfe" + integrity sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" @@ -9274,7 +9652,7 @@ selfsigned@^1.9.1: dependencies: node-forge "0.9.0" -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -9420,6 +9798,19 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sinon@^7.5.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.5.0.tgz#e9488ea466070ea908fd44a3d6478fd4923c67ec" + integrity sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q== + dependencies: + "@sinonjs/commons" "^1.4.0" + "@sinonjs/formatio" "^3.2.1" + "@sinonjs/samsam" "^3.3.3" + diff "^3.5.0" + lolex "^4.2.0" + nise "^1.5.2" + supports-color "^5.5.0" + sisteransi@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb" @@ -9755,6 +10146,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string.prototype.trim@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.0.tgz#75a729b10cfc1be439543dae442129459ce61e3d" + integrity sha512-9EIjYD/WdlvLpn987+ctkLf0FfvBefOCuiEr2henD8X+7jfwPnyvTdmW8OJhj5p+M0/96mBdynLWkxUr+rHlpg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.13.0" + function-bind "^1.1.1" + string.prototype.trimleft@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" @@ -9865,7 +10265,7 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -10167,6 +10567,11 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + type-fest@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2"