From d15f3aefc8a3b5dc7a88437a19bd31e500ccac56 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Feb 2026 20:40:55 +0000
Subject: [PATCH 1/4] Initial plan
From c3dabec6edfb0889ee5fab1cf5f55338f4355be2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Feb 2026 20:43:51 +0000
Subject: [PATCH 2/4] Add tests for decreasing fillValue and animation state
handling
Co-authored-by: jkhusanov <25942541+jkhusanov@users.noreply.github.com>
---
src/__tests__/SegmentedArc.spec.js | 71 ++++++++++++++++++++++++++++++
1 file changed, 71 insertions(+)
diff --git a/src/__tests__/SegmentedArc.spec.js b/src/__tests__/SegmentedArc.spec.js
index f5d517f..ea4cf9d 100644
--- a/src/__tests__/SegmentedArc.spec.js
+++ b/src/__tests__/SegmentedArc.spec.js
@@ -323,6 +323,77 @@ describe('SegmentedArc', () => {
expect(secondCallToValue).toBeGreaterThan(firstCallToValue);
});
+ it('re-runs animation when fillValue decreases dynamically', () => {
+ Animated.timing.mockReturnValue({ start: jest.fn(cb => cb && cb()) });
+
+ wrapper = render();
+ expect(Animated.timing).toHaveBeenCalledTimes(1);
+
+ const firstCallToValue = Animated.timing.mock.calls[0][1].toValue;
+
+ Animated.timing.mockClear();
+ Animated.timing.mockReturnValue({ start: jest.fn(cb => cb && cb()) });
+
+ wrapper.rerender();
+ expect(Animated.timing).toHaveBeenCalledTimes(1);
+
+ const secondCallToValue = Animated.timing.mock.calls[0][1].toValue;
+ expect(secondCallToValue).not.toBe(firstCallToValue);
+ expect(secondCallToValue).toBeLessThan(firstCallToValue);
+ });
+
+ it('does not trigger new animation when fillValue changes while previous animation is running', () => {
+ let animationCallback;
+ Animated.timing.mockReturnValue({
+ start: jest.fn(cb => {
+ animationCallback = cb;
+ })
+ });
+
+ wrapper = render();
+ expect(Animated.timing).toHaveBeenCalledTimes(1);
+
+ Animated.timing.mockClear();
+
+ // Rerender with new fillValue before first animation completes
+ wrapper.rerender();
+
+ // Animation should not be triggered because previous one is still running
+ expect(Animated.timing).not.toHaveBeenCalled();
+ });
+
+ it('triggers animation with latest fillValue after previous animation completes', () => {
+ let animationCallback;
+ Animated.timing.mockReturnValue({
+ start: jest.fn(cb => {
+ animationCallback = cb;
+ })
+ });
+
+ wrapper = render();
+ expect(Animated.timing).toHaveBeenCalledTimes(1);
+
+ const firstCallToValue = Animated.timing.mock.calls[0][1].toValue;
+
+ Animated.timing.mockClear();
+
+ // Complete the first animation
+ if (animationCallback) animationCallback();
+
+ Animated.timing.mockReturnValue({
+ start: jest.fn(cb => {
+ animationCallback = cb;
+ })
+ });
+
+ // Now change fillValue after animation completes - this should trigger new animation
+ wrapper.rerender();
+ expect(Animated.timing).toHaveBeenCalledTimes(1);
+
+ const secondCallToValue = Animated.timing.mock.calls[0][1].toValue;
+ expect(secondCallToValue).toBeGreaterThan(firstCallToValue);
+ });
+
it('sets the last segment for lastFilledSegment prop when fillValue is equal or greater than 100%', () => {
props.fillValue = 100;
wrapper = getWrapper(props);
From b7af65ce314e767414da2551294ec808fca93bd6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Feb 2026 20:45:18 +0000
Subject: [PATCH 3/4] Refine animation state tests based on code review
feedback
Co-authored-by: jkhusanov <25942541+jkhusanov@users.noreply.github.com>
---
src/__tests__/SegmentedArc.spec.js | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/__tests__/SegmentedArc.spec.js b/src/__tests__/SegmentedArc.spec.js
index ea4cf9d..c7acde5 100644
--- a/src/__tests__/SegmentedArc.spec.js
+++ b/src/__tests__/SegmentedArc.spec.js
@@ -362,7 +362,7 @@ describe('SegmentedArc', () => {
expect(Animated.timing).not.toHaveBeenCalled();
});
- it('triggers animation with latest fillValue after previous animation completes', () => {
+ it('triggers animation after previous animation completes', () => {
let animationCallback;
Animated.timing.mockReturnValue({
start: jest.fn(cb => {
@@ -375,11 +375,10 @@ describe('SegmentedArc', () => {
const firstCallToValue = Animated.timing.mock.calls[0][1].toValue;
- Animated.timing.mockClear();
-
// Complete the first animation
if (animationCallback) animationCallback();
+ Animated.timing.mockClear();
Animated.timing.mockReturnValue({
start: jest.fn(cb => {
animationCallback = cb;
From 0db82f053bacae13e139726785c7a0694e585dc1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 23 Feb 2026 19:18:33 +0000
Subject: [PATCH 4/4] Sync with animation-fix base branch and add decreasing
fillValue test
Co-authored-by: jkhusanov <25942541+jkhusanov@users.noreply.github.com>
---
src/SegmentedArc.js | 33 ++++++++++---
src/__tests__/SegmentedArc.spec.js | 78 ++++++++++++++----------------
2 files changed, 61 insertions(+), 50 deletions(-)
diff --git a/src/SegmentedArc.js b/src/SegmentedArc.js
index a7e96f1..c9d310d 100644
--- a/src/SegmentedArc.js
+++ b/src/SegmentedArc.js
@@ -49,7 +49,7 @@ export const SegmentedArc = ({
}
const [arcAnimatedValue] = useState(new Animated.Value(0));
- const animationRunning = useRef(false);
+ const currentAnimation = useRef(null);
useShowSegmentedArcWarnings({ segments: segmentsProps });
const { dataErrors, segments, fillValue, filledArcWidth, emptyArcWidth, spaceBetweenSegments, arcDegree, radius } =
@@ -138,19 +138,38 @@ export const SegmentedArc = ({
useEffect(() => {
if (!lastFilledSegment) return;
- if (animationRunning.current) return;
if (!isAnimated) return;
- animationRunning.current = true;
- Animated.timing(arcAnimatedValue, {
+
+ // Cancel any in-flight animation to prevent dropped updates
+ if (currentAnimation.current) {
+ currentAnimation.current.stop();
+ }
+
+ const animation = Animated.timing(arcAnimatedValue, {
toValue: lastFilledSegment.filled,
duration: animationDuration,
delay: animationDelay,
useNativeDriver: false,
easing: Easing.out(Easing.ease)
- }).start(() => {
- animationRunning.current = false;
});
- }, [lastFilledSegment.filled]);
+
+ currentAnimation.current = animation;
+
+ animation.start(({ finished }) => {
+ // Only clear if this is still the current animation AND it finished successfully (not stopped)
+ if (currentAnimation.current === animation && finished) {
+ currentAnimation.current = null;
+ }
+ });
+
+ // Cleanup: stop this specific animation if it's still running
+ return () => {
+ if (currentAnimation.current === animation) {
+ animation.stop();
+ currentAnimation.current = null;
+ }
+ };
+ }, [lastFilledSegment.filled, animationDuration, animationDelay, isAnimated]);
if (arcs.length === 0) {
return null;
diff --git a/src/__tests__/SegmentedArc.spec.js b/src/__tests__/SegmentedArc.spec.js
index c7acde5..da0c47b 100644
--- a/src/__tests__/SegmentedArc.spec.js
+++ b/src/__tests__/SegmentedArc.spec.js
@@ -42,11 +42,16 @@ describe('SegmentedArc', () => {
return render();
};
+ const createCompletedAnimationMock = () => ({
+ start: jest.fn(cb => cb && cb({ finished: true })),
+ stop: jest.fn()
+ });
+
beforeEach(() => {
Animated.timing = jest.fn();
Easing.out = jest.fn();
Easing.ease = jest.fn();
- Animated.timing.mockReturnValue({ start: jest.fn() });
+ Animated.timing.mockReturnValue({ start: jest.fn(), stop: jest.fn() });
jest.spyOn(console, 'warn').mockImplementation();
props = { segments, fillValue: 50 };
});
@@ -305,7 +310,7 @@ describe('SegmentedArc', () => {
});
it('re-runs animation when fillValue changes dynamically', () => {
- Animated.timing.mockReturnValue({ start: jest.fn(cb => cb && cb()) });
+ Animated.timing.mockReturnValue(createCompletedAnimationMock());
wrapper = render();
expect(Animated.timing).toHaveBeenCalledTimes(1);
@@ -313,7 +318,7 @@ describe('SegmentedArc', () => {
const firstCallToValue = Animated.timing.mock.calls[0][1].toValue;
Animated.timing.mockClear();
- Animated.timing.mockReturnValue({ start: jest.fn(cb => cb && cb()) });
+ Animated.timing.mockReturnValue(createCompletedAnimationMock());
wrapper.rerender();
expect(Animated.timing).toHaveBeenCalledTimes(1);
@@ -324,7 +329,7 @@ describe('SegmentedArc', () => {
});
it('re-runs animation when fillValue decreases dynamically', () => {
- Animated.timing.mockReturnValue({ start: jest.fn(cb => cb && cb()) });
+ Animated.timing.mockReturnValue(createCompletedAnimationMock());
wrapper = render();
expect(Animated.timing).toHaveBeenCalledTimes(1);
@@ -332,7 +337,7 @@ describe('SegmentedArc', () => {
const firstCallToValue = Animated.timing.mock.calls[0][1].toValue;
Animated.timing.mockClear();
- Animated.timing.mockReturnValue({ start: jest.fn(cb => cb && cb()) });
+ Animated.timing.mockReturnValue(createCompletedAnimationMock());
wrapper.rerender();
expect(Animated.timing).toHaveBeenCalledTimes(1);
@@ -342,55 +347,42 @@ describe('SegmentedArc', () => {
expect(secondCallToValue).toBeLessThan(firstCallToValue);
});
- it('does not trigger new animation when fillValue changes while previous animation is running', () => {
- let animationCallback;
- Animated.timing.mockReturnValue({
- start: jest.fn(cb => {
- animationCallback = cb;
- })
+ it('cancels in-flight animation when fillValue changes before animation completes', () => {
+ let firstAnimationCallback;
+ const mockStop = jest.fn();
+ const mockStart = jest.fn(cb => {
+ firstAnimationCallback = cb;
});
+ Animated.timing.mockReturnValue({ start: mockStart, stop: mockStop });
wrapper = render();
expect(Animated.timing).toHaveBeenCalledTimes(1);
+ expect(mockStart).toHaveBeenCalledTimes(1);
+ // Simulate fillValue changing before animation completes
Animated.timing.mockClear();
+ const newMockStop = jest.fn();
+ const newMockStart = jest.fn();
+ Animated.timing.mockReturnValue({ start: newMockStart, stop: newMockStop });
- // Rerender with new fillValue before first animation completes
- wrapper.rerender();
-
- // Animation should not be triggered because previous one is still running
- expect(Animated.timing).not.toHaveBeenCalled();
- });
-
- it('triggers animation after previous animation completes', () => {
- let animationCallback;
- Animated.timing.mockReturnValue({
- start: jest.fn(cb => {
- animationCallback = cb;
- })
- });
+ wrapper.rerender();
- wrapper = render();
+ // Verify that the previous animation was stopped
+ expect(mockStop).toHaveBeenCalled();
+ // Verify that a new animation was started
expect(Animated.timing).toHaveBeenCalledTimes(1);
+ expect(newMockStart).toHaveBeenCalledTimes(1);
- const firstCallToValue = Animated.timing.mock.calls[0][1].toValue;
-
- // Complete the first animation
- if (animationCallback) animationCallback();
-
- Animated.timing.mockClear();
- Animated.timing.mockReturnValue({
- start: jest.fn(cb => {
- animationCallback = cb;
- })
- });
-
- // Now change fillValue after animation completes - this should trigger new animation
- wrapper.rerender();
- expect(Animated.timing).toHaveBeenCalledTimes(1);
+ // Simulate the stopped animation's callback executing with finished: false
+ // This should NOT clear currentAnimation (because finished is false)
+ if (firstAnimationCallback) {
+ firstAnimationCallback({ finished: false });
+ }
- const secondCallToValue = Animated.timing.mock.calls[0][1].toValue;
- expect(secondCallToValue).toBeGreaterThan(firstCallToValue);
+ // Verify that the new animation is still active by unmounting and checking
+ // that its stop method is called
+ wrapper.unmount();
+ expect(newMockStop).toHaveBeenCalled();
});
it('sets the last segment for lastFilledSegment prop when fillValue is equal or greater than 100%', () => {