diff --git a/Libraries/Animated/__tests__/Animated-test.js b/Libraries/Animated/__tests__/Animated-test.js index eba75d254dc551..190c1beac95d93 100644 --- a/Libraries/Animated/__tests__/Animated-test.js +++ b/Libraries/Animated/__tests__/Animated-test.js @@ -65,18 +65,6 @@ describe('Animated tests', () => { style: [ { backgroundColor: 'red', - opacity: anim, - shadowOffset: { - width: anim, - height: anim, - }, - transform: [ - {translate: [translateAnim, translateAnim]}, - {translateX: translateAnim}, - {scale: anim}, - ], - }, - { opacity: 0, transform: [{translate: [100, 100]}, {translateX: 100}, {scale: 0}], shadowOffset: { @@ -101,18 +89,6 @@ describe('Animated tests', () => { style: [ { backgroundColor: 'red', - opacity: anim, - shadowOffset: { - width: anim, - height: anim, - }, - transform: [ - {translate: [translateAnim, translateAnim]}, - {translateX: translateAnim}, - {scale: anim}, - ], - }, - { opacity: 0.5, transform: [ {translate: [150, 150]}, @@ -205,7 +181,7 @@ describe('Animated tests', () => { , ); - expect(testRenderer.toJSON().props.style[1].opacity).toEqual(0); + expect(testRenderer.toJSON().props.style[0].opacity).toEqual(0); Animated.timing(opacity, { toValue: 1, @@ -213,7 +189,7 @@ describe('Animated tests', () => { useNativeDriver: false, }).start(); - expect(testRenderer.toJSON().props.style[1].opacity).toEqual(1); + expect(testRenderer.toJSON().props.style[0].opacity).toEqual(1); }); it('warns if `useNativeDriver` is missing', () => { @@ -857,12 +833,6 @@ describe('Animated tests', () => { expect(node.__getValue()).toEqual({ style: [ - { - top: vecLayout.top, - left: vecLayout.left, - opacity, - transform: vec.getTranslateTransform(), - }, { opacity: 0.2, transform: [{translateX: 0}, {translateY: 0}], @@ -882,12 +852,6 @@ describe('Animated tests', () => { expect(node.__getValue()).toEqual({ style: [ - { - top: vecLayout.top, - left: vecLayout.left, - opacity, - transform: vec.getTranslateTransform(), - }, { opacity: 0.8, transform: [{translateX: 42}, {translateY: 1492}], @@ -988,13 +952,6 @@ describe('Animated tests', () => { expect(listener).toBeCalledWith({value: 137}); expect(view.__getValue()).toEqual({ style: [ - { - transform: [ - { - translateX: value4, - }, - ], - }, { transform: [ { @@ -1091,10 +1048,6 @@ describe('Animated tests', () => { expect(node.__getValue()).toEqual({ style: [ - { - backgroundColor: color, - transform: [{scale}], - }, { backgroundColor: 'rgba(255, 0, 0, 1)', transform: [{scale: 2}], @@ -1109,10 +1062,6 @@ describe('Animated tests', () => { expect(callback.mock.calls.length).toBe(4); expect(node.__getValue()).toEqual({ style: [ - { - backgroundColor: color, - transform: [{scale}], - }, { backgroundColor: 'rgba(11, 22, 33, 0.5)', transform: [{scale: 1.5}], diff --git a/Libraries/Animated/__tests__/Animated-web-test.js b/Libraries/Animated/__tests__/Animated-web-test.js new file mode 100644 index 00000000000000..a66bb16c1d9a32 --- /dev/null +++ b/Libraries/Animated/__tests__/Animated-web-test.js @@ -0,0 +1,601 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @oncall react_native + */ + +import * as React from 'react'; +import TestRenderer from 'react-test-renderer'; + +let Animated = require('../Animated').default; +let AnimatedProps = require('../nodes/AnimatedProps').default; + +jest.mock('../../BatchedBridge/NativeModules', () => ({ + NativeAnimatedModule: {}, + PlatformConstants: { + getConstants() { + return {}; + }, + }, +})); + +jest.mock('../../Utilities/Platform', () => { + return {OS: 'web'}; +}); + +describe('Animated tests', () => { + beforeEach(() => { + jest.resetModules(); + }); + + describe('Animated', () => { + it('works end to end', () => { + const anim = new Animated.Value(0); + const translateAnim = anim.interpolate({ + inputRange: [0, 1], + outputRange: [100, 200], + }); + + const callback = jest.fn(); + + const node = new AnimatedProps( + { + style: { + backgroundColor: 'red', + opacity: anim, + transform: [ + { + translate: [translateAnim, translateAnim], + }, + { + translateX: translateAnim, + }, + {scale: anim}, + ], + shadowOffset: { + width: anim, + height: anim, + }, + }, + }, + callback, + ); + + expect(node.__getValue()).toEqual({ + style: [ + { + backgroundColor: 'red', + opacity: anim, + shadowOffset: { + width: anim, + height: anim, + }, + transform: [ + {translate: [translateAnim, translateAnim]}, + {translateX: translateAnim}, + {scale: anim}, + ], + }, + { + opacity: 0, + transform: [{translate: [100, 100]}, {translateX: 100}, {scale: 0}], + shadowOffset: { + width: 0, + height: 0, + }, + }, + ], + }); + + expect(anim.__getChildren().length).toBe(0); + + node.__attach(); + + expect(anim.__getChildren().length).toBe(3); + + anim.setValue(0.5); + + expect(callback).toBeCalled(); + + expect(node.__getValue()).toEqual({ + style: [ + { + backgroundColor: 'red', + opacity: anim, + shadowOffset: { + width: anim, + height: anim, + }, + transform: [ + {translate: [translateAnim, translateAnim]}, + {translateX: translateAnim}, + {scale: anim}, + ], + }, + { + opacity: 0.5, + transform: [ + {translate: [150, 150]}, + {translateX: 150}, + {scale: 0.5}, + ], + shadowOffset: { + width: 0.5, + height: 0.5, + }, + }, + ], + }); + + node.__detach(); + expect(anim.__getChildren().length).toBe(0); + + anim.setValue(1); + expect(callback.mock.calls.length).toBe(1); + }); + + it('does not discard initial style', () => { + const value1 = new Animated.Value(1); + const scale = value1.interpolate({ + inputRange: [0, 1], + outputRange: [1, 2], + }); + const callback = jest.fn(); + const node = new AnimatedProps( + { + style: { + transform: [ + { + scale, + }, + ], + }, + }, + callback, + ); + + expect(node.__getValue()).toEqual({ + style: [ + { + transform: [{scale}], + }, + { + transform: [{scale: 2}], + }, + ], + }); + + node.__attach(); + expect(callback.mock.calls.length).toBe(0); + value1.setValue(0.5); + expect(callback.mock.calls.length).toBe(1); + expect(node.__getValue()).toEqual({ + style: [ + { + transform: [{scale}], + }, + { + transform: [{scale: 1.5}], + }, + ], + }); + + node.__detach(); + }); + + it('does not detach on updates', () => { + const opacity = new Animated.Value(0); + opacity.__detach = jest.fn(); + + const root = TestRenderer.create(); + expect(opacity.__detach).not.toBeCalled(); + + root.update(); + expect(opacity.__detach).not.toBeCalled(); + + root.unmount(); + expect(opacity.__detach).toBeCalled(); + }); + + it('stops animation when detached', () => { + const opacity = new Animated.Value(0); + const callback = jest.fn(); + + const root = TestRenderer.create(); + + Animated.timing(opacity, { + toValue: 10, + duration: 1000, + useNativeDriver: false, + }).start(callback); + + root.unmount(); + + expect(callback).toBeCalledWith({finished: false}); + }); + + it('triggers callback when spring is at rest', () => { + const anim = new Animated.Value(0); + const callback = jest.fn(); + Animated.spring(anim, { + toValue: 0, + velocity: 0, + useNativeDriver: false, + }).start(callback); + expect(callback).toBeCalled(); + }); + + it('send toValue when a critically damped spring stops', () => { + const anim = new Animated.Value(0); + const listener = jest.fn(); + anim.addListener(listener); + Animated.spring(anim, { + stiffness: 8000, + damping: 2000, + toValue: 15, + useNativeDriver: false, + }).start(); + jest.runAllTimers(); + const lastValue = + listener.mock.calls[listener.mock.calls.length - 2][0].value; + expect(lastValue).not.toBe(15); + expect(lastValue).toBeCloseTo(15); + expect(anim.__getValue()).toBe(15); + }); + + it('convert to JSON', () => { + expect(JSON.stringify(new Animated.Value(10))).toBe('10'); + }); + + it('bypasses `setNativeProps` in test environments', () => { + const opacity = new Animated.Value(0); + + const testRenderer = TestRenderer.create( + , + ); + + expect(testRenderer.toJSON().props.style[1].opacity).toEqual(0); + + Animated.timing(opacity, { + toValue: 1, + duration: 0, + useNativeDriver: false, + }).start(); + + expect(testRenderer.toJSON().props.style[1].opacity).toEqual(1); + }); + + it('warns if `useNativeDriver` is missing', () => { + jest.spyOn(console, 'warn').mockImplementationOnce(() => {}); + + Animated.spring(new Animated.Value(0), { + toValue: 0, + velocity: 0, + // useNativeDriver + }).start(); + + expect(console.warn).toBeCalledWith( + 'Animated: `useNativeDriver` was not specified. This is a required option and must be explicitly set to `true` or `false`', + ); + console.warn.mockRestore(); + }); + }); + + describe('Animated Vectors', () => { + it('should animate vectors', () => { + const vec = new Animated.ValueXY(); + const vecLayout = vec.getLayout(); + const opacity = vec.x.interpolate({ + inputRange: [0, 42], + outputRange: [0.2, 0.8], + }); + + const callback = jest.fn(); + + const node = new AnimatedProps( + { + style: { + opacity, + transform: vec.getTranslateTransform(), + ...vecLayout, + }, + }, + callback, + ); + + expect(node.__getValue()).toEqual({ + style: [ + { + top: vecLayout.top, + left: vecLayout.left, + opacity, + transform: vec.getTranslateTransform(), + }, + { + opacity: 0.2, + transform: [{translateX: 0}, {translateY: 0}], + left: 0, + top: 0, + }, + ], + }); + + node.__attach(); + + expect(callback.mock.calls.length).toBe(0); + + vec.setValue({x: 42, y: 1492}); + + expect(callback.mock.calls.length).toBe(2); // once each for x, y + + expect(node.__getValue()).toEqual({ + style: [ + { + top: vecLayout.top, + left: vecLayout.left, + opacity, + transform: vec.getTranslateTransform(), + }, + { + opacity: 0.8, + transform: [{translateX: 42}, {translateY: 1492}], + left: 42, + top: 1492, + }, + ], + }); + + node.__detach(); + + vec.setValue({x: 1, y: 1}); + expect(callback.mock.calls.length).toBe(2); + }); + + it('should track vectors', () => { + const value1 = new Animated.ValueXY(); + const value2 = new Animated.ValueXY(); + Animated.timing(value2, { + toValue: value1, + duration: 0, + useNativeDriver: false, + }).start(); + value1.setValue({x: 42, y: 1492}); + expect(value2.__getValue()).toEqual({x: 42, y: 1492}); + + // Make sure tracking keeps working (see stopTogether in ParallelConfig used + // by maybeVectorAnim). + value1.setValue({x: 3, y: 4}); + expect(value2.__getValue()).toEqual({x: 3, y: 4}); + }); + + it('should track with springs', () => { + const value1 = new Animated.ValueXY(); + const value2 = new Animated.ValueXY(); + Animated.spring(value2, { + toValue: value1, + tension: 3000, // faster spring for faster test + friction: 60, + useNativeDriver: false, + }).start(); + value1.setValue({x: 1, y: 1}); + jest.runAllTimers(); + expect(Math.round(value2.__getValue().x)).toEqual(1); + expect(Math.round(value2.__getValue().y)).toEqual(1); + value1.setValue({x: 2, y: 2}); + jest.runAllTimers(); + expect(Math.round(value2.__getValue().x)).toEqual(2); + expect(Math.round(value2.__getValue().y)).toEqual(2); + }); + }); + + describe('Animated Listeners', () => { + it('should get updates', () => { + const value1 = new Animated.Value(0); + const listener = jest.fn(); + const id = value1.addListener(listener); + value1.setValue(42); + expect(listener.mock.calls.length).toBe(1); + expect(listener).toBeCalledWith({value: 42}); + expect(value1.__getValue()).toBe(42); + value1.setValue(7); + expect(listener.mock.calls.length).toBe(2); + expect(listener).toBeCalledWith({value: 7}); + expect(value1.__getValue()).toBe(7); + value1.removeListener(id); + value1.setValue(1492); + expect(listener.mock.calls.length).toBe(2); + expect(value1.__getValue()).toBe(1492); + }); + + it('should get updates for derived animated nodes', () => { + const value1 = new Animated.Value(40); + const value2 = new Animated.Value(50); + const value3 = new Animated.Value(0); + const value4 = Animated.add(value3, Animated.multiply(value1, value2)); + const callback = jest.fn(); + const view = new AnimatedProps( + { + style: { + transform: [ + { + translateX: value4, + }, + ], + }, + }, + callback, + ); + view.__attach(); + const listener = jest.fn(); + const id = value4.addListener(listener); + value3.setValue(137); + expect(listener.mock.calls.length).toBe(1); + expect(listener).toBeCalledWith({value: 2137}); + value1.setValue(0); + expect(listener.mock.calls.length).toBe(2); + expect(listener).toBeCalledWith({value: 137}); + expect(view.__getValue()).toEqual({ + style: [ + { + transform: [ + { + translateX: value4, + }, + ], + }, + { + transform: [ + { + translateX: 137, + }, + ], + }, + ], + }); + value4.removeListener(id); + value1.setValue(40); + expect(listener.mock.calls.length).toBe(2); + expect(value4.__getValue()).toBe(2137); + }); + + it('should removeAll', () => { + const value1 = new Animated.Value(0); + const listener = jest.fn(); + [1, 2, 3, 4].forEach(() => value1.addListener(listener)); + value1.setValue(42); + expect(listener.mock.calls.length).toBe(4); + expect(listener).toBeCalledWith({value: 42}); + value1.removeAllListeners(); + value1.setValue(7); + expect(listener.mock.calls.length).toBe(4); + }); + }); + + describe('Animated Colors', () => { + it('should normalize colors', () => { + let color = new Animated.Color(); + expect(color.__getValue()).toEqual('rgba(0, 0, 0, 1)'); + + color = new Animated.Color({r: 11, g: 22, b: 33, a: 1.0}); + expect(color.__getValue()).toEqual('rgba(11, 22, 33, 1)'); + + color = new Animated.Color('rgba(255, 0, 0, 1.0)'); + expect(color.__getValue()).toEqual('rgba(255, 0, 0, 1)'); + + color = new Animated.Color('#ff0000ff'); + expect(color.__getValue()).toEqual('rgba(255, 0, 0, 1)'); + + color = new Animated.Color('red'); + expect(color.__getValue()).toEqual('rgba(255, 0, 0, 1)'); + + color = new Animated.Color({ + r: new Animated.Value(255), + g: new Animated.Value(0), + b: new Animated.Value(0), + a: new Animated.Value(1.0), + }); + expect(color.__getValue()).toEqual('rgba(255, 0, 0, 1)'); + + color = new Animated.Color('unknown'); + expect(color.__getValue()).toEqual('rgba(0, 0, 0, 1)'); + + color = new Animated.Color({key: 'value'}); + expect(color.__getValue()).toEqual('rgba(0, 0, 0, 1)'); + }); + + it('should animate colors', () => { + const color = new Animated.Color({r: 255, g: 0, b: 0, a: 1.0}); + const scale = color.a.interpolate({ + inputRange: [0, 1], + outputRange: [1, 2], + }); + const callback = jest.fn(); + const node = new AnimatedProps( + { + style: { + backgroundColor: color, + transform: [ + { + scale, + }, + ], + }, + }, + callback, + ); + + expect(node.__getValue()).toEqual({ + style: [ + { + backgroundColor: color, + transform: [{scale}], + }, + { + backgroundColor: 'rgba(255, 0, 0, 1)', + transform: [{scale: 2}], + }, + ], + }); + + node.__attach(); + expect(callback.mock.calls.length).toBe(0); + + color.setValue({r: 11, g: 22, b: 33, a: 0.5}); + expect(callback.mock.calls.length).toBe(4); + expect(node.__getValue()).toEqual({ + style: [ + { + backgroundColor: color, + transform: [{scale}], + }, + { + backgroundColor: 'rgba(11, 22, 33, 0.5)', + transform: [{scale: 1.5}], + }, + ], + }); + + node.__detach(); + color.setValue({r: 255, g: 0, b: 0, a: 1.0}); + expect(callback.mock.calls.length).toBe(4); + }); + + it('should track colors', () => { + const color1 = new Animated.Color(); + const color2 = new Animated.Color(); + Animated.timing(color2, { + toValue: color1, + duration: 0, + useNativeDriver: false, + }).start(); + color1.setValue({r: 11, g: 22, b: 33, a: 0.5}); + expect(color2.__getValue()).toEqual('rgba(11, 22, 33, 0.5)'); + + // Make sure tracking keeps working (see stopTogether in ParallelConfig used + // by maybeVectorAnim). + color1.setValue({r: 255, g: 0, b: 0, a: 1.0}); + expect(color2.__getValue()).toEqual('rgba(255, 0, 0, 1)'); + }); + + it('should track with springs', () => { + const color1 = new Animated.Color(); + const color2 = new Animated.Color(); + Animated.spring(color2, { + toValue: color1, + tension: 3000, // faster spring for faster test + friction: 60, + useNativeDriver: false, + }).start(); + color1.setValue({r: 11, g: 22, b: 33, a: 0.5}); + jest.runAllTimers(); + expect(color2.__getValue()).toEqual('rgba(11, 22, 33, 0.5)'); + color1.setValue({r: 44, g: 55, b: 66, a: 0.0}); + jest.runAllTimers(); + expect(color2.__getValue()).toEqual('rgba(44, 55, 66, 0)'); + }); + }); +}); diff --git a/Libraries/Animated/nodes/AnimatedStyle.js b/Libraries/Animated/nodes/AnimatedStyle.js index 8d3b77c60ddfe5..2703f6af07d5da 100644 --- a/Libraries/Animated/nodes/AnimatedStyle.js +++ b/Libraries/Animated/nodes/AnimatedStyle.js @@ -13,6 +13,7 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; import flattenStyle from '../../StyleSheet/flattenStyle'; +import Platform from '../../Utilities/Platform'; import NativeAnimatedHelper from '../NativeAnimatedHelper'; import AnimatedNode from './AnimatedNode'; import AnimatedTransform from './AnimatedTransform'; @@ -62,7 +63,16 @@ export default class AnimatedStyle extends AnimatedWithChildren { } __getValue(): Array { - return [this._inputStyle, this._walkStyleAndGetValues(this._style)]; + if (Platform.OS === 'web') { + return [this._inputStyle, this._walkStyleAndGetValues(this._style)]; + } + + return [ + flattenStyle([ + this._inputStyle, + this._walkStyleAndGetValues(this._style), + ]), + ]; } // Recursively get animated values for nested styles (like iOS's shadowOffset) diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap index 2bc694a0291552..dcdbfe4f1aecb4 100644 --- a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap +++ b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap @@ -31,13 +31,7 @@ exports[`TouchableOpacity renders correctly 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { - "0": Array [ - undefined, - Object { - "opacity": 1, - }, - ], - "1": Object { + "0": Object { "opacity": 1, }, } @@ -80,13 +74,7 @@ exports[`TouchableOpacity renders in disabled state when a disabled prop is pass onStartShouldSetResponder={[Function]} style={ Object { - "0": Array [ - undefined, - Object { - "opacity": 1, - }, - ], - "1": Object { + "0": Object { "opacity": 1, }, } @@ -129,13 +117,7 @@ exports[`TouchableOpacity renders in disabled state when a key disabled in acces onStartShouldSetResponder={[Function]} style={ Object { - "0": Array [ - undefined, - Object { - "opacity": 1, - }, - ], - "1": Object { + "0": Object { "opacity": 1, }, } diff --git a/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap b/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap index 87f6ae8c37646c..6fa7ea06f15e22 100644 --- a/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap +++ b/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap @@ -32,13 +32,7 @@ exports[`