Skip to content
Snippets Groups Projects
Commit 127e1a96 authored by Paul Kaplan's avatar Paul Kaplan
Browse files

Add container test for slider prompt

A few modifications were made while making this test:
- Change the "defaultMin/Max" to just be called "min/max", and pass the state through to make the inputs a controlled component. It is best to make form elements controlled since it makes it more clear how data flows.
- Force the passed in data to always be of type `number`, and transform it in the constructor to always be of type `string`, and change the onOk to transform it back to a number. With inputs, it is best to be very explicit about data types, and make it clear where you are using strings and where you are using numbers.
- Do not reset the state to `0` when the field is blank, that makes typing into it very hard (only applies after making it a controlled field)
- Use consistent booleans. Since the VM uses `isDiscrete`, change it so we do not use any opposite flags (like `decimal`) for clarity.

Writing tests for this really helped me understand the behavior. If you read through the test descriptions and see anything you did not intend, let me know.
parent 438e0f9c
Branches
Tags
No related merge requests found
......@@ -40,13 +40,13 @@ const SliderPromptComponent = props => (
<Box>
<input
className={styles.minInput}
defaultValue={props.defaultMinValue}
name={props.intl.formatMessage(messages.minValue)}
pattern="-?[0-9]*(\.[0-9]+)?"
type="text"
onChange={props.onChangeMin}
onFocus={props.onFocus}
onKeyPress={props.onKeyPress}
pattern="-?[0-9]*(\.[0-9]+)?"
type="text"
value={props.minValue}
/>
</Box>
<Box className={styles.label}>
......@@ -55,13 +55,13 @@ const SliderPromptComponent = props => (
<Box>
<input
className={styles.maxInput}
defaultValue={props.defaultMaxValue}
name={props.intl.formatMessage(messages.maxValue)}
pattern="-?[0-9]*(\.[0-9]+)?"
type="text"
onChange={props.onChangeMax}
onFocus={props.onFocus}
onKeyPress={props.onKeyPress}
pattern="-?[0-9]*(\.[0-9]+)?"
type="text"
value={props.maxValue}
/>
</Box>
<Box className={styles.buttonRow}>
......@@ -91,9 +91,9 @@ const SliderPromptComponent = props => (
);
SliderPromptComponent.propTypes = {
defaultMaxValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
defaultMinValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
intl: intlShape,
maxValue: PropTypes.string,
minValue: PropTypes.string,
onCancel: PropTypes.func.isRequired,
onChangeMax: PropTypes.func.isRequired,
onChangeMin: PropTypes.func.isRequired,
......
......@@ -148,14 +148,14 @@ class Monitor extends React.Component {
handleSliderPromptOpen () {
this.setState({sliderPrompt: true});
}
handleSliderPromptOk (min, max, decimal) {
const realMin = Math.min(parseFloat(min), parseFloat(max));
const realMax = Math.max(parseFloat(min), parseFloat(max));
handleSliderPromptOk (min, max, isDiscrete) {
const realMin = Math.min(min, max);
const realMax = Math.max(min, max);
this.props.vm.runtime.requestUpdateMonitor(Map({
id: this.props.id,
sliderMin: realMin,
sliderMax: realMax,
isDiscrete: !decimal
isDiscrete: isDiscrete
}));
this.handleSliderPromptClose();
}
......@@ -190,9 +190,9 @@ class Monitor extends React.Component {
return (
<React.Fragment>
{this.state.sliderPrompt && <SliderPrompt
defaultMaxValue={parseFloat(this.props.max)}
defaultMinValue={parseFloat(this.props.min)}
isDiscrete={this.props.isDiscrete}
maxValue={parseFloat(this.props.max)}
minValue={parseFloat(this.props.min)}
onCancel={this.handleSliderPromptClose}
onOk={this.handleSliderPromptOk}
/>}
......
......@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import bindAll from 'lodash.bindall';
import SliderPromptComponent from '../components/slider-prompt/slider-prompt.jsx';
import log from '../lib/log';
class SliderPrompt extends React.Component {
constructor (props) {
......@@ -13,15 +12,17 @@ class SliderPrompt extends React.Component {
'handleChangeMin',
'handleChangeMax',
'handleKeyPress',
'validate',
'checkMustDecimal',
'floaty'
'validates',
'shouldBeDiscrete'
]);
const {isDiscrete, minValue, maxValue} = this.props;
this.state = {
minValue: this.props.defaultMinValue,
maxValue: this.props.defaultMaxValue
// For internal use, convert values to strings based on isDiscrete
// This is because `<input />` always returns values as strings.
minValue: isDiscrete ? minValue.toFixed(0) : minValue.toFixed(2),
maxValue: isDiscrete ? maxValue.toFixed(0) : maxValue.toFixed(2)
};
this.decimal = false;
}
handleKeyPress (event) {
if (event.key === 'Enter') this.handleOk();
......@@ -30,43 +31,36 @@ class SliderPrompt extends React.Component {
event.target.select();
}
handleOk () {
if (!this.validate()) {
const {minValue, maxValue} = this.state;
if (!this.validates(minValue, maxValue)) {
this.props.onCancel();
return;
}
this.props.onOk(this.state.minValue, this.state.maxValue,
this.checkMustDecimal(this.state.minValue, this.state.maxValue));
this.props.onOk(
parseFloat(minValue),
parseFloat(maxValue),
this.shouldBeDiscrete(minValue, maxValue));
}
handleCancel () {
this.props.onCancel();
}
floaty (value) {
const decimal = this.checkMustDecimal(this.state.minValue, this.state.maxValue) || !this.props.isDiscrete;
return `${value}${Number.isInteger(parseFloat(value)) && decimal ? '.0' : ''}`;
}
validate () {
log.log(`${this.state.minValue} ${this.state.maxValue}`);
return isFinite(this.state.minValue) && isFinite(this.state.maxValue);
}
checkMustDecimal (min, max) {
if (min === '' || max === '') return false;
return this.decimal ||
!Number.isInteger(parseFloat(min)) ||
!Number.isInteger(parseFloat(max));
}
handleChangeMin (e) {
this.decimal = e.target.value.includes('.');
this.setState({minValue: e.target.value ? e.target.value : 0});
this.setState({minValue: e.target.value});
}
handleChangeMax (e) {
this.decimal = e.target.value.includes('.');
this.setState({maxValue: e.target.value ? e.target.value : 0});
this.setState({maxValue: e.target.value});
}
shouldBeDiscrete (min, max) {
return min.indexOf('.') + max.indexOf('.') === -2; // Both -1
}
validates (min, max) {
return isFinite(min) && isFinite(max);
}
render () {
return (
<SliderPromptComponent
defaultMaxValue={this.floaty(this.props.defaultMaxValue)}
defaultMinValue={this.floaty(this.props.defaultMinValue)}
maxValue={this.state.maxValue}
minValue={this.state.minValue}
onCancel={this.handleCancel}
onChangeMax={this.handleChangeMax}
onChangeMin={this.handleChangeMin}
......@@ -79,16 +73,16 @@ class SliderPrompt extends React.Component {
}
SliderPrompt.propTypes = {
defaultMaxValue: PropTypes.number,
defaultMinValue: PropTypes.number,
isDiscrete: PropTypes.bool,
maxValue: PropTypes.number,
minValue: PropTypes.number,
onCancel: PropTypes.func.isRequired,
onOk: PropTypes.func.isRequired
};
SliderPrompt.defaultProps = {
defaultMaxValue: 100,
defaultMinValue: 0,
maxValue: 100,
minValue: 0,
isDiscrete: true
};
......
import React from 'react';
import {shallow} from 'enzyme';
import SliderPrompt from '../../../src/containers/slider-prompt.jsx';
import SliderPromptComponent from '../../../src/components/slider-prompt/slider-prompt.jsx';
describe('Slider Prompt Container', () => {
let onCancel;
let onOk;
beforeEach(() => {
onCancel = jest.fn();
onOk = jest.fn();
});
test('Min/max are shown with decimal when isDiscrete is false', () => {
const wrapper = shallow(
<SliderPrompt
isDiscrete={false}
maxValue={100}
minValue={0}
onCancel={onCancel}
onOk={onOk}
/>
);
const componentProps = wrapper.find(SliderPromptComponent).props();
expect(componentProps.minValue).toBe('0.00');
expect(componentProps.maxValue).toBe('100.00');
});
test('Min/max are NOT shown with decimal when isDiscrete is true', () => {
const wrapper = shallow(
<SliderPrompt
isDiscrete
maxValue={100}
minValue={0}
onCancel={onCancel}
onOk={onOk}
/>
);
const componentProps = wrapper.find(SliderPromptComponent).props();
expect(componentProps.minValue).toBe('0');
expect(componentProps.maxValue).toBe('100');
});
test('Entering a number with a decimal submits with isDiscrete=false', () => {
const wrapper = shallow(
<SliderPrompt
isDiscrete
maxValue={100}
minValue={0}
onCancel={onCancel}
onOk={onOk}
/>
);
const componentProps = wrapper.find(SliderPromptComponent).props();
componentProps.onChangeMin({target: {value: '1.0'}});
componentProps.onOk();
expect(onOk).toBeCalledWith(1, 100, false);
});
test('Entering integers submits with isDiscrete=true', () => {
const wrapper = shallow(
<SliderPrompt
isDiscrete={false}
maxValue={100.1}
minValue={12.32}
onCancel={onCancel}
onOk={onOk}
/>
);
const componentProps = wrapper.find(SliderPromptComponent).props();
componentProps.onChangeMin({target: {value: '1'}});
componentProps.onChangeMax({target: {value: '2'}});
componentProps.onOk();
expect(onOk).toBeCalledWith(1, 2, true);
});
test('Enter button submits the form', () => {
const wrapper = shallow(
<SliderPrompt
isDiscrete={false}
maxValue={100.1}
minValue={12.32}
onCancel={onCancel}
onOk={onOk}
/>
);
const componentProps = wrapper.find(SliderPromptComponent).props();
componentProps.onChangeMin({target: {value: '1'}});
componentProps.onChangeMax({target: {value: '2'}});
componentProps.onKeyPress({key: 'Enter'});
expect(onOk).toBeCalledWith(1, 2, true);
});
test('Validates number-ness before submitting', () => {
const wrapper = shallow(
<SliderPrompt
isDiscrete={false}
maxValue={100.1}
minValue={12.32}
onCancel={onCancel}
onOk={onOk}
/>
);
const componentProps = wrapper.find(SliderPromptComponent).props();
componentProps.onChangeMin({target: {value: 'hello'}});
componentProps.onOk();
expect(onOk).not.toBeCalled();
expect(onCancel).toBeCalled();
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment