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);
+ });
+});