diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 155884b..e8ab399 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,8 @@ jobs: run: pnpm install - name: ESLint Checks run: pnpm lint + - name: Jest Tests + run: pnpm exec jest --runInBand flow: runs-on: ubuntu-latest strategy: diff --git a/js/SegmentedControl.js b/js/SegmentedControl.js index 952d0fc..1c1c95a 100644 --- a/js/SegmentedControl.js +++ b/js/SegmentedControl.js @@ -47,6 +47,10 @@ const SegmentedControl = ({ const ref = React.useRef>(null); const handleChange = (index: number) => { + if (index === selectedIndex) { + return; + } + // mocks iOS's nativeEvent const event: any = { nativeEvent: { diff --git a/js/__tests__/SegmentedControl.test.js b/js/__tests__/SegmentedControl.test.js new file mode 100644 index 0000000..71a06ce --- /dev/null +++ b/js/__tests__/SegmentedControl.test.js @@ -0,0 +1,99 @@ +/* eslint-env jest */ + +import React from 'react'; +import {TouchableOpacity} from 'react-native'; +import renderer, {act} from 'react-test-renderer'; +import SegmentedControl from '../SegmentedControl.js'; + +describe('Android and web selection callbacks', () => { + let tree; + let props; + + beforeEach(() => { + props = { + values: ['One', 'Two'], + selectedIndex: 0, + onChange: jest.fn(), + onValueChange: jest.fn(), + }; + }); + + afterEach(() => { + act(() => tree?.unmount()); + }); + + function render(overrides = {}) { + props = {...props, ...overrides}; + act(() => { + tree = renderer.create(); + }); + } + + function press(index) { + act(() => { + tree.root.findAllByType(TouchableOpacity)[index].props.onPress(); + }); + } + + it.each([0, 1])('does not notify for selected index %i', (selectedIndex) => { + render({selectedIndex}); + press(selectedIndex); + expect(props.onChange).not.toHaveBeenCalled(); + expect(props.onValueChange).not.toHaveBeenCalled(); + }); + + it('notifies both callbacks once with the newly selected segment', () => { + render(); + press(1); + expect(props.onChange).toHaveBeenCalledTimes(1); + expect(props.onChange).toHaveBeenCalledWith({ + nativeEvent: {value: 'Two', selectedSegmentIndex: 1}, + }); + expect(props.onValueChange).toHaveBeenCalledTimes(1); + expect(props.onValueChange).toHaveBeenCalledWith('Two'); + }); + + it('uses the latest controlled selection after a prop update', () => { + render(); + act(() => { + tree.update(); + }); + press(1); + expect(props.onChange).not.toHaveBeenCalled(); + expect(props.onValueChange).not.toHaveBeenCalled(); + press(0); + expect(props.onChange).toHaveBeenCalledTimes(1); + expect(props.onChange).toHaveBeenCalledWith({ + nativeEvent: {value: 'One', selectedSegmentIndex: 0}, + }); + expect(props.onValueChange).toHaveBeenCalledTimes(1); + expect(props.onValueChange).toHaveBeenCalledWith('One'); + }); + + it('compares indexes when two segments have the same value', () => { + render({values: ['Same', 'Same']}); + press(1); + expect(props.onChange).toHaveBeenCalledWith({ + nativeEvent: {value: 'Same', selectedSegmentIndex: 1}, + }); + expect(props.onValueChange).toHaveBeenCalledWith('Same'); + }); + + it.each([undefined, null, -1])( + 'allows a first selection when selectedIndex is %s', + (selectedIndex) => { + render({selectedIndex}); + press(0); + expect(props.onChange).toHaveBeenCalledTimes(1); + expect(props.onValueChange).toHaveBeenCalledWith('One'); + }, + ); + + it('allows optional callbacks to be omitted', () => { + render({onChange: undefined, onValueChange: undefined}); + expect(() => { + press(0); + press(1); + }).not.toThrow(); + }); +});