UNPKG

rheostat

Version:

Rheostat is a www, mobile, and accessible slider component built with React

727 lines (584 loc) 26.3 kB
import { shallow, mount } from 'enzyme'; import React from 'react'; import createReactClass from 'create-react-class'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import sinon from 'sinon'; import { assert } from 'chai'; import has from 'has'; import DirectionProvider, { DIRECTIONS } from 'react-with-direction/dist/DirectionProvider'; import Slider from '../src/Slider'; import DefaultHandle from '../src/DefaultHandle'; import DefaultProgressBar from '../src/DefaultProgressBar'; import { KEYS, VERTICAL, } from '../src/constants/SliderConstants'; const { WITH_DOM } = process.env; const describeWithDOM = WITH_DOM === '1' ? describe : describe.skip; function testKeys(slider, tests) { Object.keys(tests).forEach((key) => { const keyCode = KEYS[key]; const pos = tests[key]; assert(slider.getNextPositionForKey(0, keyCode) === pos, `${key}: ${pos}%`); }); } describeWithDOM('<Slider />', () => { describe('render', () => { it('should render the slider with one handle by default', () => { const wrapper = shallow(<Slider />).dive().dive(); assert(wrapper.find(DefaultHandle).length === 1, 'no values one handle'); }); it('should render the slider with a single handle', () => { const wrapper = shallow(<Slider values={[1]} handle={DefaultHandle} />).dive().dive(); assert(wrapper.find(DefaultHandle).length === 1, 'one handle is present'); }); it('should render the slider with as many handles as values', () => { const wrapper = shallow(<Slider values={[0, 25, 50, 75, 100]} />).dive().dive(); assert(wrapper.find(DefaultHandle).length === 5, 'five handles are present'); }); it('should render the slider with a bar', () => { const wrapper = shallow(<Slider />).dive().dive(); assert(wrapper.find(DefaultProgressBar).length === 1, 'the bar is present'); }); it('renders pits if they are provided', () => { const pitRender = sinon.stub().returns(<div />); const PitComponent = createReactClass({ render: pitRender, }); mount(<Slider pitComponent={PitComponent} pitPoints={[0, 20]} />); assert.isTrue(pitRender.calledTwice, 'two pits were rendered, one for each point'); }); it('renders pits if they are provided', () => { const pitRender = sinon.stub().returns(<div />); const PitComponent = createReactClass({ render: pitRender, }); mount(( <Slider orientation="vertical" pitComponent={PitComponent} pitPoints={[10]} /> )); assert.isTrue(pitRender.calledOnce, 'one pit was rendered vertically'); }); it('doesn\'t re-renders pits when value are changed', () => { const pitRender = sinon.stub().returns(<div />); const PitComponent = createReactClass({ mixins: [PureRenderMixin], render: pitRender, }); mount(<Slider pitComponent={PitComponent} pitPoints={[20]} values={[10]} />); assert.isTrue(pitRender.calledOnce, 'one pit was rendered only once'); }); it('should not throw react errors on disabled', () => { const slider = mount(<Slider />); slider.setProps({ disabled: true }); }); it('should pass undefined to key and mouse event handlers on disabled', () => { const slider = mount(<Slider disabled />); assert.isUndefined(slider.find(DefaultHandle).first().props().onKeyDown, 'onKeyDown is undefined'); assert.isUndefined(slider.find(DefaultHandle).first().props().onMouseDown, 'onMouseDown is undefined'); assert.isUndefined(slider.find(DefaultHandle).first().props().onTouchStart, 'onTouchStart is undefined'); }); it('should pass functions to key and mouse event handlers', () => { const slider = mount(<Slider />); assert.isFunction(slider.find(DefaultHandle).first().props().onKeyDown, 'onKeyDown is function'); assert.isFunction(slider.find(DefaultHandle).first().props().onMouseDown, 'onMouseDown is function'); assert.isFunction(slider.find(DefaultHandle).first().props().onTouchStart, 'onTouchStart is function'); }); }); describe('componentWillReceiveProps', () => { it('should not call onChange twice if values are the same as what is in state', () => { const onChange = sinon.spy(); const slider = shallow(<Slider onChange={onChange} values={[0]} />).dive().dive(); // programatically change values like if the slider was dragged slider.setState({ values: [10] }); slider.setProps({ values: [10] }); assert(onChange.callCount === 0, 'onChange was not called'); }); it('should not update values if we are sliding', () => { const onChange = sinon.spy(); const slider = shallow(<Slider onChange={onChange} values={[0]} />).dive().dive(); slider.setState({ slidingIndex: 0 }); slider.setProps({ values: [50] }); assert(onChange.callCount === 0, 'updateNewValues was not called'); }); it('should not update values if they are the same', () => { const onChange = sinon.spy(); const slider = mount(<Slider onChange={onChange} values={[50]} />); slider.setProps({ values: [50] }); assert(onChange.callCount === 0, 'updateNewValues was not called'); }); it('should update values when they change', () => { const slider = shallow(<Slider values={[50]} />).dive().dive(); slider.setProps({ values: [80] }); assert.include(slider.state('values'), 80, 'new value is reflected in state'); }); it('should re-render pits when min or max are changed', () => { const pitRender = sinon.stub().returns(<div />); const PitComponent = createReactClass({ mixins: [PureRenderMixin], render: pitRender, }); const slider = mount(<Slider pitComponent={PitComponent} pitPoints={[20]} />); slider.setProps({ min: 30 }); slider.setProps({ max: 60 }); assert.isTrue(pitRender.calledThrice, 'one pit was rendered thrice'); }); it('should re-render pits when pitPoints are changed', () => { const pitRender = sinon.stub().returns(<div />); const PitComponent = createReactClass({ mixins: [PureRenderMixin], render: pitRender, }); const slider = mount(<Slider pitComponent={PitComponent} pitPoints={[20]} />); slider.setProps({ pitPoints: [40] }); assert.isTrue(pitRender.calledTwice, 'one pit was rendered twice'); }); it('should re-render pits when orientation are changed', () => { const pitRender = sinon.stub().returns(<div />); const PitComponent = createReactClass({ mixins: [PureRenderMixin], render: pitRender, }); const slider = mount(<Slider pitComponent={PitComponent} pitPoints={[20]} />); slider.setProps({ orientation: VERTICAL }); assert.isTrue(pitRender.calledTwice, 'one pit was rendered twice'); }); it('should re-render pits when algorithm are changed', () => { const pitRender = sinon.stub().returns(<div />); const PitComponent = createReactClass({ mixins: [PureRenderMixin], render: pitRender, }); const algorithm = { getPosition: () => 20, getValue: () => 30, }; const slider = mount(<Slider pitComponent={PitComponent} pitPoints={[20]} />); slider.setProps({ algorithm }); assert.isTrue(pitRender.calledTwice, 'one pit was rendered twice'); }); it('should move the values if the min is changed to be larger', () => { const slider = shallow(<Slider values={[50]} />).dive().dive(); slider.setProps({ min: 80 }); assert.include(slider.state('values'), 80, 'values was updated'); }); it('should move the values if the max is changed to be smaller', () => { const slider = shallow(<Slider values={[50]} />).dive().dive(); slider.setProps({ max: 20 }); assert.include(slider.state('values'), 20, 'values was updated'); }); it('should add handles', () => { const slider = shallow(<Slider />).dive().dive(); assert(slider.state('values').length === 1, 'one handle exists'); assert(slider.state('handlePos').length === 1, 'one handle exists'); slider.setProps({ values: [] }); assert(slider.state('values').length === 0, 'no handles exist'); assert(slider.state('handlePos').length === 0, 'no handles exist'); slider.setProps({ values: [0, 100] }); assert(slider.state('values').length === 2, 'two handles exist'); assert(slider.state('handlePos').length === 2, 'two handles exist'); }); }); describe('getSliderBoundingBox', () => { it('returns null if the ref to the handle container node is not yet set', () => { const slider = shallow(<Slider />).dive().dive().instance(); assert.isNull(slider.getSliderBoundingBox(), 'handle container ref is not yet set'); }); }); }); describe('Slider API', () => { describe('getPublicState', () => { it('should only return min, max, and values from public state', () => { const slider = shallow(<Slider />).dive().dive().instance(); const state = slider.getPublicState(); assert.isTrue(has(state, 'max'), 'max exists'); assert.isTrue(has(state, 'min'), 'min exists'); assert.isTrue(has(state, 'values'), 'values exists'); assert(Object.keys(state).length === 3, 'only 3 properties are present'); }); }); describe('getProgressStyle', () => { it('should get correct style for horizontal slider', () => { const slider = shallow(<Slider />).dive().dive().instance(); const style = slider.getProgressStyle(0); assert.isTrue(has(style, 'left'), 'left exists'); assert.isTrue(has(style, 'width'), 'width exists'); assert(Object.keys(style).length === 2, 'only two properties exist'); }); it('should get correct style for single handle at 0%', () => { const slider = shallow(<Slider />).dive().dive().instance(); const style = slider.getProgressStyle(0); assert(style.left === 0, 'progress bar is at 0 because it is single handle'); assert(style.width === '0%', 'progress bar is at 0%'); }); it('should get correct style for single handle at 50%', () => { const slider = shallow(<Slider values={[50]} max={100} />).dive().dive().instance(); const style = slider.getProgressStyle(0); assert(style.width === '50%', 'progress bar is at 50'); }); it('should get correct style for second handle at 50%', () => { const slider = shallow(<Slider values={[50, 100]} max={100} />).dive().dive().instance(); const style = slider.getProgressStyle(1); assert(style.left === '50%', 'progress bar starts at 50%'); assert(style.width === '50%', 'progress bar spans 50%'); }); it('should get correct style for vertical slider', () => { const slider = shallow(<Slider orientation={VERTICAL} />).dive().dive().instance(); const style = slider.getProgressStyle(0); assert.isTrue(has(style, 'top'), 'top exists'); assert.isTrue(has(style, 'height'), 'height exists'); assert(Object.keys(style).length === 2, 'only two properties exist'); }); it('should get correct style for second handle and vertical slider', () => { const slider = shallow(<Slider values={[50, 100]} orientation={VERTICAL} />).dive().dive().instance(); const style = slider.getProgressStyle(1); assert(style.top === '50%', 'progress bar starts at 50%'); assert(style.height === '50%', 'progress bar spans 50%'); }); }); describe('getMinValue', () => { it('should get the min value for single handle', () => { const slider = shallow(<Slider values={[20]} min={10} />).dive().dive().instance(); assert(slider.getMinValue(0) === 10, 'the minimum possible value is 10'); }); it('should get the min value for second handle', () => { const slider = shallow(<Slider values={[20, 40]} min={0} />).dive().dive().instance(); assert(slider.getMinValue(1) === 20, 'the minimum possible value is 20'); }); }); describe('getMaxValue', () => { it('should get the max value for single handle', () => { const slider = shallow(<Slider values={[20]} max={50} />).dive().dive().instance(); assert(slider.getMaxValue(0) === 50, 'the maximum possible value is 50'); }); it('should get the max value for two handles', () => { const slider = shallow(<Slider values={[20, 30]} />).dive().dive().instance(); assert(slider.getMaxValue(0) === 30, 'the maximum possible value is 30'); }); }); describe('getClosestSnapPoint', () => { it('should get the closest value inside points given a value', () => { const slider = shallow(<Slider snapPoints={[0, 50]} />).dive().dive().instance(); assert(slider.getClosestSnapPoint(25) === 50, 'the closest point is 50'); assert(slider.getClosestSnapPoint(24) === 0, 'the closest point is 0'); }); it('should return the value if points does not exist', () => { const slider = shallow(<Slider />).dive().dive().instance(); assert(slider.getClosestSnapPoint(42) === 42, 'the closest point is 42'); }); }); describe('getSnapPosition', () => { it('should return the position if snap is false', () => { const slider = shallow(<Slider />).dive().dive().instance(); assert(slider.getSnapPosition(20) === 20, 'position is 20'); }); it('should snap to the closest value and give its position', () => { const slider = shallow(<Slider snap snapPoints={[0, 25, 50, 75, 100]} />).dive().dive().instance(); assert(slider.getSnapPosition(20) === 25, 'position is at 25%'); assert(slider.getSnapPosition(96) === 100, 'position is at 100%'); assert(slider.getSnapPosition(55) === 50, 'position is at 50%'); }); }); describe('getNextPositionForKey', () => { it('should try to advance 1% when pressing left, right, up or down', () => { const slider = shallow(<Slider values={[50]} />).dive().dive().instance(); testKeys(slider, { LEFT: 49, RIGHT: 51, UP: 51, DOWN: 49, }); }); it('should try to advance up to 10% when pressing page up/down', () => { const slider = shallow(<Slider values={[50]} />).dive().dive().instance(); testKeys(slider, { PAGE_UP: 60, PAGE_DOWN: 40, }); }); it('should reach the start/end when pressing home/end', () => { const slider = shallow(<Slider values={[50]} />).dive().dive().instance(); testKeys(slider, { HOME: 0, END: 100, }); }); it('overflows min', () => { const slider = shallow(<Slider values={[0]} />).dive().dive().instance(); testKeys(slider, { PAGE_DOWN: -10, LEFT: -1, HOME: 0, }); }); it('overflows max', () => { const slider = shallow(<Slider values={[100]} />).dive().dive().instance(); testKeys(slider, { END: 100, RIGHT: 101, PAGE_UP: 110, }); }); it('should increment by value on a really small scale', () => { const slider = shallow(<Slider values={[2]} max={5} />).dive().dive().instance(); testKeys(slider, { END: 100, RIGHT: 60, PAGE_UP: 60, PAGE_DOWN: 20, LEFT: 20, HOME: 0, }); }); it('should handle large scales well', () => { const slider = shallow(<Slider values={[5e8]} max={1e9} />).dive().dive().instance(); testKeys(slider, { END: 100, RIGHT: 51, PAGE_UP: 60, PAGE_DOWN: 40, LEFT: 49, HOME: 0, }); }); it('should snap to a value if snap is set', () => { const slider = shallow(<Slider snap snapPoints={[10, 20, 40, 60, 80]} values={[40]} />).dive().dive().instance(); testKeys(slider, { END: 80, RIGHT: 60, PAGE_UP: 60, PAGE_DOWN: 20, LEFT: 20, HOME: 10, }); }); it('should not overflow min with snap', () => { const slider = shallow(<Slider snap snapPoints={[10, 20, 40, 60, 80]} values={[10]} />).dive().dive().instance(); testKeys(slider, { LEFT: 10, PAGE_DOWN: 10, HOME: 10, }); }); it('should not overflow max with snap', () => { const slider = shallow(<Slider snap snapPoints={[10, 20, 40, 60, 80]} values={[80]} />).dive().dive().instance(); testKeys(slider, { RIGHT: 80, PAGE_UP: 80, END: 80, }); }); it('should return null for escape', () => { const slider = shallow(<Slider />).dive().dive().instance(); assert.isNull(slider.getNextPositionForKey(0, KEYS.ESC)); }); }); describe('need slider bounding box defined', () => { const sliderBoundingBox = { height: 15, left: 145, right: 423, top: 53.59375, width: 278, }; describe('getNextState', () => { it('should return the next state given a position and index', () => { const slider = shallow(<Slider values={[0]} />).dive().dive().instance(); sinon.stub(slider, 'getSliderBoundingBox').returns(sliderBoundingBox); const nextState = slider.getNextState(0, 50); assert(nextState.handlePos[0] === 50, 'handle is at 50%'); assert(nextState.values[0] === 50, 'the value is 50'); }); it('should return correct validated state given two handles and overflow', () => { const slider = shallow(<Slider values={[0, 20]} />).dive().dive().instance(); sinon.stub(slider, 'getSliderBoundingBox').returns(sliderBoundingBox); const nextState = slider.getNextState(0, 50); assert(nextState.handlePos[0] === 20, 'handle is at 20%'); assert(nextState.values[0] === 20, 'the value is 20'); }); it('should not overflow the boundaries', () => { const slider = shallow(<Slider values={[20]} />).dive().dive().instance(); sinon.stub(slider, 'getSliderBoundingBox').returns(sliderBoundingBox); let nextState = slider.getNextState(0, -20); assert(nextState.handlePos[0] === 0, 'handle is at 0%'); assert(nextState.values[0] === 0, 'the value is 0'); nextState = slider.getNextState(0, 120); assert(nextState.handlePos[0] === 100, 'handle is at 100%'); assert(nextState.values[0] === 100, 'the value is 100'); }); }); describe('validatePosition', () => { it('should make sure that handles respect bounds', () => { const slider = shallow(<Slider values={[50]} />).dive().dive().instance(); sinon.stub(slider, 'getSliderBoundingBox').returns(sliderBoundingBox); assert(slider.validatePosition(0, -20) === 0, 'the handle was set to the min'); assert(slider.validatePosition(0, 120) === 100, 'the handle was set to the max'); assert(slider.validatePosition(0, 25) === 25, 'the correct position is returned'); }); it('should verify that two handles do not overlap', () => { const slider = shallow(<Slider values={[25, 75]} />).dive().dive().instance(); sinon.stub(slider, 'getSliderBoundingBox').returns(sliderBoundingBox); assert(slider.validatePosition(0, 90) === 75, 'the handle reached its own max'); assert(slider.validatePosition(1, 20) === 25, 'the handle reached its own min'); }); it('should honor getNextHandlePosition precondition', () => { const LEFT_MAX = 40; const LEFT_HANDLE_IDX = 0; const slider = shallow(<Slider values={[30]} getNextHandlePosition={ (idx, pos) => (idx === LEFT_HANDLE_IDX && pos > LEFT_MAX ? LEFT_MAX : pos) } />).dive().dive().instance(); sinon.stub(slider, 'getSliderBoundingBox').returns(sliderBoundingBox); assert(slider.validatePosition(0, 90) === 40, 'it honors the validatePosition override'); assert(slider.validatePosition(0, 39) === 39, 'accepts the default value when condition is not triggered'); }); it('should throw if getNextHandlePosition returns invalid input', () => { const nanSlider = shallow(( <Slider values={[30]} getNextHandlePosition={() => NaN} /> )).dive().dive().instance(); sinon.stub(nanSlider, 'getSliderBoundingBox').returns(sliderBoundingBox); assert.throws( () => nanSlider.validatePosition(0, 100), TypeError, 'getNextHandlePosition returned invalid position. Valid positions are floats between 0 and 100', 'it throws if a non - float is returns from getNextHandlePosition', ); const outOfBoundsSlider = shallow(( <Slider values={[30]} getNextHandlePosition={() => -100} />)).dive().dive().instance(); sinon.stub(outOfBoundsSlider, 'getSliderBoundingBox').returns(sliderBoundingBox); assert.throws( () => outOfBoundsSlider.validatePosition(0, 100), TypeError, 'getNextHandlePosition returned invalid position. Valid positions are floats between 0 and 100', 'it throws if getNextHandlePosition returns out of bounds', ); }); }); describe('canMove', () => { it('should confirm that we can move to the proposed position', () => { const slider = shallow(<Slider values={[50]} />).dive().dive().instance(); sinon.stub(slider, 'getSliderBoundingBox').returns(sliderBoundingBox); assert.isFalse(slider.canMove(0, 120), 'cannot overflow max'); assert.isFalse(slider.canMove(0, -20), 'cannot overflow min'); }); it('should not overflow the position of another handle', () => { const slider = shallow(<Slider values={[20, 60]} />).dive().dive().instance(); sinon.stub(slider, 'getSliderBoundingBox').returns(sliderBoundingBox); assert.isFalse(slider.canMove(0, 80), 'cannot overflow second handle'); assert.isFalse(slider.canMove(1, 10), 'cannot overflow first handle'); }); it('should return true if it can move to the position', () => { const slider = shallow(<Slider values={[25]} />).dive().dive().instance(); sinon.stub(slider, 'getSliderBoundingBox').returns(sliderBoundingBox); assert.isTrue(slider.canMove(0, 40), 'sure you can move here'); }); it('should return false if the ref to the handle container node is not yet set', () => { const slider = shallow(<Slider values={[50]} />).dive().dive().instance(); assert.isUndefined(slider.handleContainerNode, 'ref is not available'); assert.isFalse(slider.canMove(0, 40), 'ref is not available'); }); }); }); describe('getClosestHandle', () => { it('should return the index of the closest handle given a position', () => { const slider = shallow(<Slider values={[0, 25, 50, 75, 100]} />).dive().dive().instance(); assert(slider.getClosestHandle(55) === 2, 'the index of the handle at 50% is 2'); assert(slider.getClosestHandle(89) === 4, 'the index of the handle at 100% is 4'); assert(slider.getClosestHandle(4) === 0, 'the index of the handle at 0% is 0'); }); }); describe('validateValues', () => { it('should validate that values do not overflow', () => { const slider = shallow(<Slider values={[50]} />).dive().dive().instance(); assert(slider.validateValues([-20])[0] === 0, 'the value is set to the min'); assert(slider.validateValues([120])[0] === 100, 'the value is set to the max'); }); it('should assert that values do not overlap', () => { const slider = shallow(<Slider />).dive().dive().instance(); const newValues = slider.validateValues([80, 20]); assert(newValues[0] === 80, 'the first value is 80'); assert(newValues[1] === 80, 'the second value is 80'); }); }); describe('positionPercent', () => { it('should return correct position for horizontal orientation', () => { const box = { height: 10, left: 150, right: 200, top: 50, width: 300, }; const slider = shallow(<Slider />).dive().dive().instance(); assert.equal(slider.positionPercent(box.left, 55, box), 0, 'check returns min value'); assert.equal(slider.positionPercent(box.left + box.width / 2, 55, box), 50, 'check returns middle value'); assert.equal(slider.positionPercent(box.left + box.width, 55, box), 100, 'check returns max value'); }); it('should return correct position for vertical orientation', () => { const box = { height: 200, left: 50, right: 100, top: 50, width: 20, }; const slider = shallow(<Slider orientation="vertical" />).dive().dive().instance(); assert.equal(slider.positionPercent(55, box.top, box), 0, 'check returns min value'); assert.equal(slider.positionPercent(55, box.top + box.height / 2, box), 50, 'check returns middle value'); assert.equal(slider.positionPercent(55, box.top + box.height, box), 100, 'check returns max value'); }); it('should return correct position for horizontal orientation in RTL', () => { const box = { height: 10, left: 150, right: 200, top: 50, width: 300, }; const slider = shallow( <DirectionProvider direction={DIRECTIONS.RTL}><Slider /></DirectionProvider>, ) .find(Slider) .dive() .dive() .dive() .instance(); assert.equal(slider.positionPercent(box.left, 55, box), 100, 'check returns min value'); assert.equal(slider.positionPercent(box.left + box.width / 2, 55, box), 50, 'check returns middle value'); assert.equal(slider.positionPercent(box.left + box.width, 55, box), 0, 'check returns max value'); }); }); });