From 733e3c34380cb4554710f28ada65fbbaab50937a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20=C4=90=C3=ACnh=20Huy?= <68500260+huytdps13400@users.noreply.github.com> Date: Mon, 5 Oct 2026 02:18:46 +0700 Subject: [PATCH] fix: ignore failed segment measurements --- .github/workflows/ci.yml | 2 + js/SegmentedControl.js | 8 +- .../SegmentedControl.measurement.test.js | 108 ++++++++++++++++++ 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 js/__tests__/SegmentedControl.measurement.test.js 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..b39e725 100644 --- a/js/SegmentedControl.js +++ b/js/SegmentedControl.js @@ -83,9 +83,11 @@ const SegmentedControl = ({ React.useEffect(() => { if (ref.current) { - ref.current.measure((_x: number, _y: number, width: number) => - updateSegmentWidth(width), - ); + ref.current.measure((_x: number, _y: number, width?: number) => { + if (width !== undefined) { + updateSegmentWidth(width); + } + }); } }, [values, updateSegmentWidth]); diff --git a/js/__tests__/SegmentedControl.measurement.test.js b/js/__tests__/SegmentedControl.measurement.test.js new file mode 100644 index 0000000..8933986 --- /dev/null +++ b/js/__tests__/SegmentedControl.measurement.test.js @@ -0,0 +1,108 @@ +/* eslint-env jest */ + +import React from 'react'; +import {Animated, StyleSheet, View} from 'react-native'; +import renderer, {act} from 'react-test-renderer'; +import SegmentedControl from '../SegmentedControl.js'; + +describe('Android and web measurements', () => { + let tree; + let measurements; + let setValue; + const values = ['One', 'Two']; + + beforeEach(() => { + measurements = []; + jest.spyOn(View.prototype, 'measure').mockImplementation((callback) => { + measurements.push(callback); + }); + setValue = jest.spyOn(Animated.Value.prototype, 'setValue'); + act(() => { + tree = renderer.create( + , + ); + }); + }); + + afterEach(() => { + act(() => tree.unmount()); + jest.restoreAllMocks(); + }); + + function deliverMeasurement(...args) { + expect(measurements.length).toBeGreaterThan(0); + const callback = measurements[measurements.length - 1]; + act(() => callback(...args)); + } + + function sliders() { + return tree.root + .findAllByType(Animated.View) + .filter( + (node) => StyleSheet.flatten(node.props.style).position === 'absolute', + ); + } + + function sliderWidth() { + const matches = sliders(); + expect(matches).toHaveLength(1); + return StyleSheet.flatten(matches[0].props.style).width; + } + + it('ignores a failed measurement before the first layout', () => { + deliverMeasurement(); + expect(setValue).not.toHaveBeenCalled(); + expect(sliders()).toHaveLength(0); + }); + + it('keeps the last valid width when a later measurement fails', () => { + deliverMeasurement(0, 0, 300, 32, 0, 0); + expect(sliderWidth()).toBe(146); + setValue.mockClear(); + deliverMeasurement(); + expect(setValue).not.toHaveBeenCalled(); + expect(sliderWidth()).toBe(146); + }); + + it('does not send NaN to Animated when a failed callback arrives after unmount', () => { + const callback = measurements[0]; + act(() => tree.unmount()); + act(() => callback()); + expect(setValue).not.toHaveBeenCalled(); + }); + + it('still applies a valid zero width', () => { + deliverMeasurement(0, 0, 300, 32, 0, 0); + setValue.mockClear(); + deliverMeasurement(0, 0, 0, 32, 0, 0); + expect(setValue).toHaveBeenCalledWith(0); + expect(sliders()).toHaveLength(0); + }); + + it('positions and sizes the slider from a successful measurement', () => { + deliverMeasurement(0, 0, 300, 32, 0, 0); + expect(setValue).toHaveBeenCalledWith(150); + expect(sliderWidth()).toBe(146); + }); + + it('recalculates the width when the number of segments changes', () => { + deliverMeasurement(0, 0, 300, 32, 0, 0); + act(() => { + tree.update( + , + ); + }); + deliverMeasurement(0, 0, 300, 32, 0, 0); + expect(setValue).toHaveBeenLastCalledWith(100); + expect(sliderWidth()).toBe(96); + }); + + it('accepts a layout event after a failed measurement', () => { + deliverMeasurement(); + setValue.mockClear(); + const container = tree.root.findAllByType(View)[0]; + act(() => container.props.onLayout({nativeEvent: {layout: {width: 300}}})); + expect(setValue).toHaveBeenCalledWith(150); + expect(sliderWidth()).toBe(146); + }); +});