[Vis: Default editor] Unit tests for number list (#48412)
* Add unit tests for number list * Remove unused dependency * Fix code review comments * Refactor NumberList to set model validity in one place
This commit is contained in:
parent
17464f8612
commit
fb903f4f9f
|
@ -0,0 +1,77 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NumberList should be rendered with default set of props 1`] = `
|
||||
<Fragment>
|
||||
<NumberRow
|
||||
autoFocus={false}
|
||||
disableDelete={false}
|
||||
isInvalid={false}
|
||||
labelledbyId="numberList"
|
||||
model={
|
||||
Object {
|
||||
"id": "122",
|
||||
"isInvalid": false,
|
||||
"value": 1,
|
||||
}
|
||||
}
|
||||
onBlur={[MockFunction]}
|
||||
onChange={[Function]}
|
||||
onDelete={[Function]}
|
||||
range={
|
||||
Range {
|
||||
"max": 10,
|
||||
"maxInclusive": true,
|
||||
"min": 1,
|
||||
"minInclusive": true,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<NumberRow
|
||||
autoFocus={true}
|
||||
disableDelete={false}
|
||||
isInvalid={false}
|
||||
labelledbyId="numberList"
|
||||
model={
|
||||
Object {
|
||||
"id": "123",
|
||||
"isInvalid": false,
|
||||
"value": 2,
|
||||
}
|
||||
}
|
||||
onBlur={[MockFunction]}
|
||||
onChange={[Function]}
|
||||
onDelete={[Function]}
|
||||
range={
|
||||
Range {
|
||||
"max": 10,
|
||||
"maxInclusive": true,
|
||||
"min": 1,
|
||||
"minInclusive": true,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty
|
||||
iconType="plusInCircleFilled"
|
||||
onClick={[Function]}
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add {unitName}"
|
||||
id="common.ui.aggTypes.numberList.addUnitButtonLabel"
|
||||
values={
|
||||
Object {
|
||||
"unitName": "value",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,38 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NumberRow should be rendered with default set of props 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldNumber
|
||||
aria-labelledby="numberList"
|
||||
autoFocus={false}
|
||||
compressed={true}
|
||||
fullWidth={true}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
max={10}
|
||||
min={1}
|
||||
onBlur={[MockFunction]}
|
||||
onChange={[Function]}
|
||||
placeholder="Enter a value"
|
||||
value={1}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Remove the rank value of 1"
|
||||
color="danger"
|
||||
disabled={false}
|
||||
iconType="trash"
|
||||
onClick={[Function]}
|
||||
title="Remove the rank value of 1"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
import { NumberList, NumberListProps } from './number_list';
|
||||
import { NumberRow } from './number_row';
|
||||
|
||||
jest.mock('./number_row', () => ({
|
||||
NumberRow: () => 'NumberRow',
|
||||
}));
|
||||
|
||||
jest.mock('@elastic/eui', () => ({
|
||||
htmlIdGenerator: jest.fn(() => {
|
||||
let counter = 1;
|
||||
return () => `12${counter++}`;
|
||||
}),
|
||||
EuiSpacer: require.requireActual('@elastic/eui').EuiSpacer,
|
||||
EuiFlexItem: require.requireActual('@elastic/eui').EuiFlexItem,
|
||||
EuiButtonEmpty: require.requireActual('@elastic/eui').EuiButtonEmpty,
|
||||
EuiFormErrorText: require.requireActual('@elastic/eui').EuiFormErrorText,
|
||||
}));
|
||||
|
||||
describe('NumberList', () => {
|
||||
let defaultProps: NumberListProps;
|
||||
|
||||
beforeEach(() => {
|
||||
defaultProps = {
|
||||
labelledbyId: 'numberList',
|
||||
numberArray: [1, 2],
|
||||
range: '[1, 10]',
|
||||
showValidation: false,
|
||||
unitName: 'value',
|
||||
onChange: jest.fn(),
|
||||
setTouched: jest.fn(),
|
||||
setValidity: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
test('should be rendered with default set of props', () => {
|
||||
const comp = shallow(<NumberList {...defaultProps} />);
|
||||
|
||||
expect(comp).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show an order error', () => {
|
||||
defaultProps.numberArray = [3, 1];
|
||||
defaultProps.showValidation = true;
|
||||
const comp = mountWithIntl(<NumberList {...defaultProps} />);
|
||||
|
||||
expect(comp.find('EuiFormErrorText').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should set validity as true', () => {
|
||||
mountWithIntl(<NumberList {...defaultProps} />);
|
||||
|
||||
expect(defaultProps.setValidity).lastCalledWith(true);
|
||||
});
|
||||
|
||||
test('should set validity as false when the order is invalid', () => {
|
||||
defaultProps.numberArray = [3, 2];
|
||||
const comp = mountWithIntl(<NumberList {...defaultProps} />);
|
||||
|
||||
expect(defaultProps.setValidity).lastCalledWith(false);
|
||||
|
||||
comp.setProps({ numberArray: [1, 2] });
|
||||
expect(defaultProps.setValidity).lastCalledWith(true);
|
||||
});
|
||||
|
||||
test('should set validity as false when there is an empty field', () => {
|
||||
defaultProps.numberArray = [1, 2];
|
||||
const comp = mountWithIntl(<NumberList {...defaultProps} />);
|
||||
|
||||
comp.setProps({ numberArray: [1, undefined] });
|
||||
expect(defaultProps.setValidity).lastCalledWith(false);
|
||||
});
|
||||
|
||||
test('should set 0 when number array is empty', () => {
|
||||
defaultProps.numberArray = [];
|
||||
mountWithIntl(<NumberList {...defaultProps} />);
|
||||
|
||||
expect(defaultProps.onChange).lastCalledWith([0]);
|
||||
});
|
||||
|
||||
test('should add a number', () => {
|
||||
const comp = shallow(<NumberList {...defaultProps} />);
|
||||
comp.find('EuiButtonEmpty').simulate('click');
|
||||
|
||||
expect(defaultProps.onChange).lastCalledWith([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('should remove a number', () => {
|
||||
const comp = shallow(<NumberList {...defaultProps} />);
|
||||
const row = comp.find(NumberRow).first();
|
||||
row.prop('onDelete')(row.prop('model').id);
|
||||
|
||||
expect(defaultProps.onChange).lastCalledWith([2]);
|
||||
});
|
||||
|
||||
test('should disable remove button if there is one number', () => {
|
||||
defaultProps.numberArray = [1];
|
||||
const comp = shallow(<NumberList {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
comp
|
||||
.find(NumberRow)
|
||||
.first()
|
||||
.prop('disableDelete')
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('should change value', () => {
|
||||
const comp = shallow(<NumberList {...defaultProps} />);
|
||||
const row = comp.find(NumberRow).first();
|
||||
row.prop('onChange')({ id: row.prop('model').id, value: '3' });
|
||||
|
||||
expect(defaultProps.onChange).lastCalledWith([3, 2]);
|
||||
});
|
||||
|
||||
test('should call setTouched', () => {
|
||||
const comp = shallow(<NumberList {...defaultProps} />);
|
||||
comp
|
||||
.find(NumberRow)
|
||||
.first()
|
||||
.prop('onBlur')();
|
||||
|
||||
expect(defaultProps.setTouched).toBeCalled();
|
||||
});
|
||||
});
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import React, { Fragment, useState, useEffect, useMemo, useCallback } from 'react';
|
||||
|
||||
import { EuiSpacer, EuiButtonEmpty, EuiFlexItem, EuiFormErrorText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -34,16 +34,15 @@ import {
|
|||
getUpdatedModels,
|
||||
hasInvalidValues,
|
||||
} from './utils';
|
||||
import { useValidation } from '../../agg_utils';
|
||||
|
||||
interface NumberListProps {
|
||||
export interface NumberListProps {
|
||||
labelledbyId: string;
|
||||
numberArray: Array<number | undefined>;
|
||||
range?: string;
|
||||
showValidation: boolean;
|
||||
unitName: string;
|
||||
validateAscendingOrder?: boolean;
|
||||
onBlur?(): void;
|
||||
onFocus?(): void;
|
||||
onChange(list: Array<number | undefined>): void;
|
||||
setTouched(): void;
|
||||
setValidity(isValid: boolean): void;
|
||||
|
@ -56,81 +55,84 @@ function NumberList({
|
|||
showValidation,
|
||||
unitName,
|
||||
validateAscendingOrder = true,
|
||||
onBlur,
|
||||
onFocus,
|
||||
onChange,
|
||||
setTouched,
|
||||
setValidity,
|
||||
}: NumberListProps) {
|
||||
const numberRange = getRange(range);
|
||||
const numberRange = useMemo(() => getRange(range), [range]);
|
||||
const [models, setModels] = useState(getInitModelList(numberArray));
|
||||
const [ascendingError, setAscendingError] = useState(EMPTY_STRING);
|
||||
|
||||
// responsible for discarding changes
|
||||
// set up validity for each model
|
||||
useEffect(() => {
|
||||
const updatedModels = getUpdatedModels(numberArray, models, numberRange);
|
||||
let id: number | undefined;
|
||||
if (validateAscendingOrder) {
|
||||
const isOrderValid = validateOrder(updatedModels);
|
||||
const { isValidOrder, modelIndex } = validateOrder(numberArray);
|
||||
id = isValidOrder ? undefined : modelIndex;
|
||||
setAscendingError(
|
||||
isOrderValid
|
||||
? i18n.translate('common.ui.aggTypes.numberList.invalidAscOrderErrorMessage', {
|
||||
isValidOrder
|
||||
? EMPTY_STRING
|
||||
: i18n.translate('common.ui.aggTypes.numberList.invalidAscOrderErrorMessage', {
|
||||
defaultMessage: 'The values should be in ascending order.',
|
||||
})
|
||||
: EMPTY_STRING
|
||||
);
|
||||
}
|
||||
setModels(updatedModels);
|
||||
}, [numberArray]);
|
||||
setModels(state => getUpdatedModels(numberArray, state, numberRange, id));
|
||||
}, [numberArray, numberRange, validateAscendingOrder]);
|
||||
|
||||
// responsible for setting up an initial value ([0]) when there is no default value
|
||||
useEffect(() => {
|
||||
setValidity(!hasInvalidValues(models));
|
||||
}, [models]);
|
||||
|
||||
// resposible for setting up an initial value ([0]) when there is no default value
|
||||
useEffect(() => {
|
||||
onChange(models.map(({ value }) => (value === EMPTY_STRING ? undefined : value)));
|
||||
if (!numberArray.length) {
|
||||
onChange([models[0].value as number]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onChangeValue = ({ id, value }: { id: string; value: string }) => {
|
||||
const parsedValue = parse(value);
|
||||
const { isValid, errors } = validateValue(parsedValue, numberRange);
|
||||
setValidity(isValid);
|
||||
const isValid = !hasInvalidValues(models);
|
||||
useValidation(setValidity, isValid);
|
||||
|
||||
const currentModel = models.find(model => model.id === id);
|
||||
if (currentModel) {
|
||||
currentModel.value = parsedValue;
|
||||
currentModel.isInvalid = !isValid;
|
||||
currentModel.errors = errors;
|
||||
}
|
||||
const onUpdate = useCallback(
|
||||
(modelList: NumberRowModel[]) => {
|
||||
setModels(modelList);
|
||||
onChange(modelList.map(({ value }) => (value === EMPTY_STRING ? undefined : value)));
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
onUpdate(models);
|
||||
};
|
||||
const onChangeValue = useCallback(
|
||||
({ id, value }: { id: string; value: string }) => {
|
||||
const parsedValue = parse(value);
|
||||
|
||||
onUpdate(
|
||||
models.map(model => {
|
||||
if (model.id === id) {
|
||||
const { isInvalid, error } = validateValue(parsedValue, numberRange);
|
||||
return {
|
||||
id,
|
||||
value: parsedValue,
|
||||
isInvalid,
|
||||
error,
|
||||
};
|
||||
}
|
||||
return model;
|
||||
})
|
||||
);
|
||||
},
|
||||
[numberRange, models, onUpdate]
|
||||
);
|
||||
|
||||
// Add an item to the end of the list
|
||||
const onAdd = () => {
|
||||
const onAdd = useCallback(() => {
|
||||
const newArray = [...models, getNextModel(models, numberRange)];
|
||||
onUpdate(newArray);
|
||||
};
|
||||
}, [models, numberRange, onUpdate]);
|
||||
|
||||
const onDelete = (id: string) => {
|
||||
const newArray = models.filter(model => model.id !== id);
|
||||
onUpdate(newArray);
|
||||
};
|
||||
|
||||
const onBlurFn = (model: NumberRowModel) => {
|
||||
if (model.value === EMPTY_STRING) {
|
||||
model.isInvalid = true;
|
||||
}
|
||||
setTouched();
|
||||
if (onBlur) {
|
||||
onBlur();
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdate = (modelList: NumberRowModel[]) => {
|
||||
setModels(modelList);
|
||||
onChange(modelList.map(({ value }) => (value === EMPTY_STRING ? undefined : value)));
|
||||
};
|
||||
const onDelete = useCallback(
|
||||
(id: string) => {
|
||||
const newArray = models.filter(model => model.id !== id);
|
||||
onUpdate(newArray);
|
||||
},
|
||||
[models, onUpdate]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -143,13 +145,12 @@ function NumberList({
|
|||
labelledbyId={labelledbyId}
|
||||
range={numberRange}
|
||||
onDelete={onDelete}
|
||||
onFocus={onFocus}
|
||||
onChange={onChangeValue}
|
||||
onBlur={() => onBlurFn(model)}
|
||||
onBlur={setTouched}
|
||||
autoFocus={models.length !== 1 && arrayIndex === models.length - 1}
|
||||
/>
|
||||
{showValidation && model.isInvalid && model.errors && model.errors.length > 0 && (
|
||||
<EuiFormErrorText>{model.errors.join('\n')}</EuiFormErrorText>
|
||||
{showValidation && model.isInvalid && model.error && (
|
||||
<EuiFormErrorText>{model.error}</EuiFormErrorText>
|
||||
)}
|
||||
{models.length - 1 !== arrayIndex && <EuiSpacer size="s" />}
|
||||
</Fragment>
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { NumberRow, NumberRowProps } from './number_row';
|
||||
|
||||
describe('NumberRow', () => {
|
||||
let defaultProps: NumberRowProps;
|
||||
|
||||
beforeEach(() => {
|
||||
defaultProps = {
|
||||
autoFocus: false,
|
||||
disableDelete: false,
|
||||
isInvalid: false,
|
||||
labelledbyId: 'numberList',
|
||||
model: { value: 1, id: '1', isInvalid: false },
|
||||
range: {
|
||||
min: 1,
|
||||
max: 10,
|
||||
minInclusive: true,
|
||||
maxInclusive: true,
|
||||
within: jest.fn(() => true),
|
||||
},
|
||||
onChange: jest.fn(),
|
||||
onBlur: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
test('should be rendered with default set of props', () => {
|
||||
const comp = shallow(<NumberRow {...defaultProps} />);
|
||||
|
||||
expect(comp).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should call onDelete', () => {
|
||||
const comp = shallow(<NumberRow {...defaultProps} />);
|
||||
comp.find('EuiButtonIcon').simulate('click');
|
||||
|
||||
expect(defaultProps.onDelete).lastCalledWith(defaultProps.model.id);
|
||||
});
|
||||
|
||||
test('should call onChange', () => {
|
||||
const comp = shallow(<NumberRow {...defaultProps} />);
|
||||
comp.find('EuiFieldNumber').prop('onChange')!({ target: { value: '5' } } as React.ChangeEvent<
|
||||
HTMLInputElement
|
||||
>);
|
||||
|
||||
expect(defaultProps.onChange).lastCalledWith({ id: defaultProps.model.id, value: '5' });
|
||||
});
|
||||
});
|
|
@ -17,13 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Range } from '../../../../../../utils/range';
|
||||
|
||||
interface NumberRowProps {
|
||||
export interface NumberRowProps {
|
||||
autoFocus: boolean;
|
||||
disableDelete: boolean;
|
||||
isInvalid: boolean;
|
||||
|
@ -31,7 +31,6 @@ interface NumberRowProps {
|
|||
model: NumberRowModel;
|
||||
range: Range;
|
||||
onBlur(): void;
|
||||
onFocus?(): void;
|
||||
onChange({ id, value }: { id: string; value: string }): void;
|
||||
onDelete(index: string): void;
|
||||
}
|
||||
|
@ -40,7 +39,7 @@ export interface NumberRowModel {
|
|||
id: string;
|
||||
isInvalid: boolean;
|
||||
value: number | '';
|
||||
errors?: string[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
function NumberRow({
|
||||
|
@ -52,7 +51,6 @@ function NumberRow({
|
|||
range,
|
||||
onBlur,
|
||||
onDelete,
|
||||
onFocus,
|
||||
onChange,
|
||||
}: NumberRowProps) {
|
||||
const deleteBtnAriaLabel = i18n.translate(
|
||||
|
@ -63,11 +61,16 @@ function NumberRow({
|
|||
}
|
||||
);
|
||||
|
||||
const onValueChanged = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChange({
|
||||
value: event.target.value,
|
||||
id: model.id,
|
||||
});
|
||||
const onValueChanged = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChange({
|
||||
value: event.target.value,
|
||||
id: model.id,
|
||||
}),
|
||||
[onChange, model.id]
|
||||
);
|
||||
|
||||
const onDeleteFn = useCallback(() => onDelete(model.id), [onDelete, model.id]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="s">
|
||||
|
@ -81,7 +84,6 @@ function NumberRow({
|
|||
defaultMessage: 'Enter a value',
|
||||
})}
|
||||
onChange={onValueChanged}
|
||||
onFocus={onFocus}
|
||||
value={model.value}
|
||||
fullWidth={true}
|
||||
min={range.min}
|
||||
|
@ -95,7 +97,7 @@ function NumberRow({
|
|||
title={deleteBtnAriaLabel}
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
onClick={() => onDelete(model.id)}
|
||||
onClick={onDeleteFn}
|
||||
disabled={disableDelete}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
getInitModelList,
|
||||
getUpdatedModels,
|
||||
validateOrder,
|
||||
hasInvalidValues,
|
||||
parse,
|
||||
validateValue,
|
||||
getNextModel,
|
||||
getRange,
|
||||
} from './utils';
|
||||
import { Range } from '../../../../../../utils/range';
|
||||
import { NumberRowModel } from './number_row';
|
||||
|
||||
describe('NumberList utils', () => {
|
||||
let modelList: NumberRowModel[];
|
||||
let range: Range;
|
||||
|
||||
beforeEach(() => {
|
||||
modelList = [{ value: 1, id: '1', isInvalid: false }, { value: 2, id: '2', isInvalid: false }];
|
||||
range = {
|
||||
min: 1,
|
||||
max: 10,
|
||||
minInclusive: true,
|
||||
maxInclusive: true,
|
||||
within: jest.fn(() => true),
|
||||
};
|
||||
});
|
||||
|
||||
describe('getInitModelList', () => {
|
||||
test('should return list with default model when number list is empty', () => {
|
||||
const models = getInitModelList([]);
|
||||
|
||||
expect(models).toEqual([{ value: 0, id: expect.any(String), isInvalid: false }]);
|
||||
});
|
||||
|
||||
test('should return model list', () => {
|
||||
const models = getInitModelList([1, undefined]);
|
||||
|
||||
expect(models).toEqual([
|
||||
{ value: 1, id: expect.any(String), isInvalid: false },
|
||||
{ value: '', id: expect.any(String), isInvalid: false },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUpdatedModels', () => {
|
||||
test('should return model list when number list is empty', () => {
|
||||
const updatedModelList = getUpdatedModels([], modelList, range);
|
||||
|
||||
expect(updatedModelList).toEqual([{ value: 0, id: expect.any(String), isInvalid: false }]);
|
||||
});
|
||||
|
||||
test('should not update model list when number list is the same', () => {
|
||||
const updatedModelList = getUpdatedModels([1, 2], modelList, range);
|
||||
|
||||
expect(updatedModelList).toEqual(modelList);
|
||||
});
|
||||
|
||||
test('should update model list when number list was changed', () => {
|
||||
const updatedModelList = getUpdatedModels([1, 3], modelList, range);
|
||||
modelList[1].value = 3;
|
||||
expect(updatedModelList).toEqual(modelList);
|
||||
});
|
||||
|
||||
test('should update model list when number list increased', () => {
|
||||
const updatedModelList = getUpdatedModels([1, 2, 3], modelList, range);
|
||||
expect(updatedModelList).toEqual([
|
||||
...modelList,
|
||||
{ value: 3, id: expect.any(String), isInvalid: false },
|
||||
]);
|
||||
});
|
||||
|
||||
test('should update model list when number list decreased', () => {
|
||||
const updatedModelList = getUpdatedModels([2], modelList, range);
|
||||
expect(updatedModelList).toEqual([{ value: 2, id: '1', isInvalid: false }]);
|
||||
});
|
||||
|
||||
test('should update model list when number list has undefined value', () => {
|
||||
const updatedModelList = getUpdatedModels([1, undefined], modelList, range);
|
||||
modelList[1].value = '';
|
||||
modelList[1].isInvalid = true;
|
||||
expect(updatedModelList).toEqual(modelList);
|
||||
});
|
||||
|
||||
test('should update model list when number order is invalid', () => {
|
||||
const updatedModelList = getUpdatedModels([1, 3, 2], modelList, range, 2);
|
||||
expect(updatedModelList).toEqual([
|
||||
modelList[0],
|
||||
{ ...modelList[1], value: 3 },
|
||||
{ value: 2, id: expect.any(String), isInvalid: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateOrder', () => {
|
||||
test('should return true when order is valid', () => {
|
||||
expect(validateOrder([1, 2])).toEqual({
|
||||
isValidOrder: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('should return true when a number is undefined', () => {
|
||||
expect(validateOrder([1, undefined])).toEqual({
|
||||
isValidOrder: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('should return false when order is invalid', () => {
|
||||
expect(validateOrder([2, 1])).toEqual({
|
||||
isValidOrder: false,
|
||||
modelIndex: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasInvalidValues', () => {
|
||||
test('should return false when there are no invalid models', () => {
|
||||
expect(hasInvalidValues(modelList)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should return true when there is an invalid model', () => {
|
||||
modelList[1].isInvalid = true;
|
||||
expect(hasInvalidValues(modelList)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
test('should return a number', () => {
|
||||
expect(parse('3')).toBe(3);
|
||||
});
|
||||
|
||||
test('should return an empty string when value is invalid', () => {
|
||||
expect(parse('')).toBe('');
|
||||
expect(parse('test')).toBe('');
|
||||
expect(parse('NaN')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateValue', () => {
|
||||
test('should return valid', () => {
|
||||
expect(validateValue(3, range)).toEqual({ isInvalid: false });
|
||||
});
|
||||
|
||||
test('should return invalid', () => {
|
||||
range.within = jest.fn(() => false);
|
||||
expect(validateValue(11, range)).toEqual({ isInvalid: true, error: expect.any(String) });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextModel', () => {
|
||||
test('should return 3 as next value', () => {
|
||||
expect(getNextModel(modelList, range)).toEqual({
|
||||
value: 3,
|
||||
id: expect.any(String),
|
||||
isInvalid: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('should return 1 as next value', () => {
|
||||
expect(getNextModel([{ value: '', id: '2', isInvalid: false }], range)).toEqual({
|
||||
value: 1,
|
||||
id: expect.any(String),
|
||||
isInvalid: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('should return 9 as next value', () => {
|
||||
expect(getNextModel([{ value: 11, id: '2', isInvalid: false }], range)).toEqual({
|
||||
value: 9,
|
||||
id: expect.any(String),
|
||||
isInvalid: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRange', () => {
|
||||
test('should return default range', () => {
|
||||
expect(getRange()).toEqual({
|
||||
min: 0,
|
||||
max: Infinity,
|
||||
maxInclusive: false,
|
||||
minInclusive: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('should return parsed range', () => {
|
||||
expect(getRange('(-Infinity, 100]')).toEqual({
|
||||
min: -Infinity,
|
||||
max: 100,
|
||||
maxInclusive: true,
|
||||
minInclusive: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('should throw an error', () => {
|
||||
expect(() => getRange('test')).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -27,6 +27,7 @@ import { NumberRowModel } from './number_row';
|
|||
const EMPTY_STRING = '';
|
||||
const defaultRange = parseRange('[0,Infinity)');
|
||||
const generateId = htmlIdGenerator();
|
||||
const defaultModel = { value: 0, id: generateId(), isInvalid: false };
|
||||
|
||||
function parse(value: string) {
|
||||
const parsedValue = parseFloat(value);
|
||||
|
@ -42,44 +43,37 @@ function getRange(range?: string): Range {
|
|||
}
|
||||
|
||||
function validateValue(value: number | '', numberRange: Range) {
|
||||
const result = {
|
||||
isValid: true,
|
||||
errors: [] as string[],
|
||||
const result: { isInvalid: boolean; error?: string } = {
|
||||
isInvalid: false,
|
||||
};
|
||||
|
||||
if (value === EMPTY_STRING) {
|
||||
result.isValid = false;
|
||||
result.isInvalid = true;
|
||||
} else if (!numberRange.within(value)) {
|
||||
result.isValid = false;
|
||||
result.errors.push(
|
||||
i18n.translate('common.ui.aggTypes.numberList.invalidRangeErrorMessage', {
|
||||
defaultMessage: 'The value should be in the range of {min} to {max}.',
|
||||
values: { min: numberRange.min, max: numberRange.max },
|
||||
})
|
||||
);
|
||||
result.isInvalid = true;
|
||||
result.error = i18n.translate('common.ui.aggTypes.numberList.invalidRangeErrorMessage', {
|
||||
defaultMessage: 'The value should be in the range of {min} to {max}.',
|
||||
values: { min: numberRange.min, max: numberRange.max },
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateOrder(list: NumberRowModel[]) {
|
||||
let isInvalidOrder = false;
|
||||
list.forEach((model, index, array) => {
|
||||
function validateOrder(list: Array<number | undefined>) {
|
||||
const result: { isValidOrder: boolean; modelIndex?: number } = {
|
||||
isValidOrder: true,
|
||||
};
|
||||
|
||||
list.forEach((inputValue, index, array) => {
|
||||
const previousModel = array[index - 1];
|
||||
if (previousModel && model.value !== EMPTY_STRING) {
|
||||
const isInvalidOrderOfItem = model.value <= previousModel.value;
|
||||
|
||||
if (!model.isInvalid && isInvalidOrderOfItem) {
|
||||
model.isInvalid = true;
|
||||
}
|
||||
|
||||
if (isInvalidOrderOfItem) {
|
||||
isInvalidOrder = true;
|
||||
}
|
||||
if (previousModel !== undefined && inputValue !== undefined && inputValue <= previousModel) {
|
||||
result.isValidOrder = false;
|
||||
result.modelIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
return isInvalidOrder;
|
||||
return result;
|
||||
}
|
||||
|
||||
function getNextModel(list: NumberRowModel[], range: Range): NumberRowModel {
|
||||
|
@ -104,26 +98,27 @@ function getInitModelList(list: Array<number | undefined>): NumberRowModel[] {
|
|||
id: generateId(),
|
||||
isInvalid: false,
|
||||
}))
|
||||
: [{ value: 0, id: generateId(), isInvalid: false }];
|
||||
: [defaultModel];
|
||||
}
|
||||
|
||||
function getUpdatedModels(
|
||||
numberList: Array<number | undefined>,
|
||||
modelList: NumberRowModel[],
|
||||
numberRange: Range
|
||||
numberRange: Range,
|
||||
invalidOrderModelIndex?: number
|
||||
): NumberRowModel[] {
|
||||
if (!numberList.length) {
|
||||
return modelList;
|
||||
return [defaultModel];
|
||||
}
|
||||
return numberList.map((number, index) => {
|
||||
const model = modelList[index] || { id: generateId() };
|
||||
const newValue: NumberRowModel['value'] = number === undefined ? EMPTY_STRING : number;
|
||||
const { isValid, errors } = validateValue(newValue, numberRange);
|
||||
const { isInvalid, error } = validateValue(newValue, numberRange);
|
||||
return {
|
||||
...model,
|
||||
value: newValue,
|
||||
isInvalid: !isValid,
|
||||
errors,
|
||||
isInvalid: invalidOrderModelIndex === index ? true : isInvalid,
|
||||
error,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ function PercentileRanksEditor({
|
|||
});
|
||||
const [isValid, setIsValid] = useState(true);
|
||||
|
||||
const setModelValidy = (isListValid: boolean) => {
|
||||
const setModelValidity = (isListValid: boolean) => {
|
||||
setIsValid(isListValid);
|
||||
setValidity(isListValid);
|
||||
};
|
||||
|
@ -62,7 +62,7 @@ function PercentileRanksEditor({
|
|||
showValidation={showValidation}
|
||||
onChange={setValue}
|
||||
setTouched={setTouched}
|
||||
setValidity={setModelValidy}
|
||||
setValidity={setModelValidity}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
|
|
@ -37,7 +37,7 @@ function PercentilesEditor({
|
|||
});
|
||||
const [isValid, setIsValid] = useState(true);
|
||||
|
||||
const setModelValidy = (isListValid: boolean) => {
|
||||
const setModelValidity = (isListValid: boolean) => {
|
||||
setIsValid(isListValid);
|
||||
setValidity(isListValid);
|
||||
};
|
||||
|
@ -62,7 +62,7 @@ function PercentilesEditor({
|
|||
showValidation={showValidation}
|
||||
onChange={setValue}
|
||||
setTouched={setTouched}
|
||||
setValidity={setModelValidy}
|
||||
setValidity={setModelValidity}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue