From 66e72bb4e00aafbcb9f450ed5db261d98f99f82a Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Sun, 3 Nov 2019 18:00:15 -0800 Subject: [PATCH] Animated: Forward Ref to Component Summary: Changes `createAnimatedComponent` so that a `ref` assigned to an Animated component will now be forwarded to the internal component. Previously, a ref to the internal component was accessed using the `getNode` method. The `getNode` method is now deprecated and will return the same `ref` but show a deprecation error. Changelog: [General] [Changed] - Refs on an Animated component are now the internal component. The `getNode` call has been deprecated. Reviewed By: TheSavior Differential Revision: D18290474 fbshipit-source-id: 5849809583a17624a89071db8be1282a12caedf3 --- .../Animated/src/__tests__/Animated-test.js | 48 +- .../src/__tests__/AnimatedNative-test.js | 460 ++++++++---------- .../__snapshots__/Animated-test.js.snap | 21 - .../Animated/src/createAnimatedComponent.js | 44 +- ...ogBoxInspectorSourceMapStatus-test.js.snap | 4 +- .../js/examples/Touchable/TouchableExample.js | 51 ++ 6 files changed, 311 insertions(+), 317 deletions(-) delete mode 100644 Libraries/Animated/src/__tests__/__snapshots__/Animated-test.js.snap diff --git a/Libraries/Animated/src/__tests__/Animated-test.js b/Libraries/Animated/src/__tests__/Animated-test.js index f119806e16c7b7..061fd449925fac 100644 --- a/Libraries/Animated/src/__tests__/Animated-test.js +++ b/Libraries/Animated/src/__tests__/Animated-test.js @@ -95,51 +95,33 @@ describe('Animated tests', () => { }); it('does not detach on updates', () => { - const anim = new Animated.Value(0); - anim.__detach = jest.fn(); + const opacity = new Animated.Value(0); + opacity.__detach = jest.fn(); - const c = new Animated.View(); - c.props = { - style: { - opacity: anim, - }, - }; - c.UNSAFE_componentWillMount(); + const root = TestRenderer.create(); + expect(opacity.__detach).not.toBeCalled(); - expect(anim.__detach).not.toBeCalled(); - c._component = {}; - c.UNSAFE_componentWillReceiveProps({ - style: { - opacity: anim, - }, - }); - expect(anim.__detach).not.toBeCalled(); + root.update(); + expect(opacity.__detach).not.toBeCalled(); - c.componentWillUnmount(); - expect(anim.__detach).toBeCalled(); + root.unmount(); + expect(opacity.__detach).toBeCalled(); }); it('stops animation when detached', () => { - const anim = new Animated.Value(0); + const opacity = new Animated.Value(0); const callback = jest.fn(); - const c = new Animated.View(); - c.props = { - style: { - opacity: anim, - }, - }; - c.UNSAFE_componentWillMount(); + const root = TestRenderer.create(); - Animated.timing(anim, { + Animated.timing(opacity, { toValue: 10, duration: 1000, useNativeDriver: false, }).start(callback); - c._component = {}; - c.componentWillUnmount(); - expect(callback).toBeCalledWith({finished: false}); + root.unmount(); + expect(callback).toBeCalledWith({finished: false}); }); @@ -198,7 +180,7 @@ describe('Animated tests', () => { , ); - expect(testRenderer.toJSON()).toMatchSnapshot(); + expect(testRenderer.toJSON().props.style.opacity).toEqual(0); Animated.timing(opacity, { toValue: 1, @@ -206,7 +188,7 @@ describe('Animated tests', () => { useNativeDriver: false, }).start(); - expect(testRenderer.toJSON()).toMatchSnapshot(); + expect(testRenderer.toJSON().props.style.opacity).toEqual(1); }); it('warns if `useNativeDriver` is missing', () => { diff --git a/Libraries/Animated/src/__tests__/AnimatedNative-test.js b/Libraries/Animated/src/__tests__/AnimatedNative-test.js index 09b087ce5d5be0..b6e34047f6cf93 100644 --- a/Libraries/Animated/src/__tests__/AnimatedNative-test.js +++ b/Libraries/Animated/src/__tests__/AnimatedNative-test.js @@ -10,18 +10,8 @@ 'use strict'; -const ClassComponentMock = class {}; -ClassComponentMock.prototype.isReactComponent = true; - jest .clearAllMocks() - .setMock('../../../Text/Text', ClassComponentMock) - .setMock('../../../Components/View/View', ClassComponentMock) - .setMock('../../../Image/Image', ClassComponentMock) - .setMock('../../../Components/ScrollView/ScrollView', ClassComponentMock) - .setMock('../../../Lists/FlatList', ClassComponentMock) - .setMock('../../../Lists/SectionList', ClassComponentMock) - .setMock('react', {Component: class {}}) .mock('../../../BatchedBridge/NativeModules', () => ({ NativeAnimatedModule: {}, PlatformConstants: { @@ -35,128 +25,107 @@ jest // findNodeHandle is imported from ReactNative so mock that whole module. .setMock('../../../Renderer/shims/ReactNative', {findNodeHandle: () => 1}); +import TestRenderer from 'react-test-renderer'; +import * as React from 'react'; + const Animated = require('../Animated'); const NativeAnimatedHelper = require('../NativeAnimatedHelper'); -function createAndMountComponent(ComponentClass, props) { - const component = new ComponentClass(); - component.props = props; - component.UNSAFE_componentWillMount(); - // Simulate that refs were set. - component._component = {}; - component.componentDidMount(); - return component; -} - describe('Native Animated', () => { - const nativeAnimatedModule = require('../NativeAnimatedModule').default; + const NativeAnimatedModule = require('../NativeAnimatedModule').default; beforeEach(() => { - nativeAnimatedModule.addAnimatedEventToView = jest.fn(); - nativeAnimatedModule.connectAnimatedNodes = jest.fn(); - nativeAnimatedModule.connectAnimatedNodeToView = jest.fn(); - nativeAnimatedModule.createAnimatedNode = jest.fn(); - nativeAnimatedModule.disconnectAnimatedNodeFromView = jest.fn(); - nativeAnimatedModule.disconnectAnimatedNodes = jest.fn(); - nativeAnimatedModule.dropAnimatedNode = jest.fn(); - nativeAnimatedModule.extractAnimatedNodeOffset = jest.fn(); - nativeAnimatedModule.flattenAnimatedNodeOffset = jest.fn(); - nativeAnimatedModule.removeAnimatedEventFromView = jest.fn(); - nativeAnimatedModule.setAnimatedNodeOffset = jest.fn(); - nativeAnimatedModule.setAnimatedNodeValue = jest.fn(); - nativeAnimatedModule.startAnimatingNode = jest.fn(); - nativeAnimatedModule.startListeningToAnimatedNodeValue = jest.fn(); - nativeAnimatedModule.stopAnimation = jest.fn(); - nativeAnimatedModule.stopListeningToAnimatedNodeValue = jest.fn(); + Object.assign(NativeAnimatedModule, { + addAnimatedEventToView: jest.fn(), + connectAnimatedNodes: jest.fn(), + connectAnimatedNodeToView: jest.fn(), + createAnimatedNode: jest.fn(), + disconnectAnimatedNodeFromView: jest.fn(), + disconnectAnimatedNodes: jest.fn(), + dropAnimatedNode: jest.fn(), + extractAnimatedNodeOffset: jest.fn(), + flattenAnimatedNodeOffset: jest.fn(), + removeAnimatedEventFromView: jest.fn(), + setAnimatedNodeOffset: jest.fn(), + setAnimatedNodeValue: jest.fn(), + startAnimatingNode: jest.fn(), + startListeningToAnimatedNodeValue: jest.fn(), + stopAnimation: jest.fn(), + stopListeningToAnimatedNodeValue: jest.fn(), + }); }); describe('Animated Value', () => { it('proxies `setValue` correctly', () => { - const anim = new Animated.Value(0); - Animated.timing(anim, { + const opacity = new Animated.Value(0); + const ref = React.createRef(null); + + Animated.timing(opacity, { toValue: 10, duration: 1000, useNativeDriver: true, }).start(); - const c = createAndMountComponent(Animated.View, { - style: { - opacity: anim, - }, - }); + TestRenderer.create(); - // We expect `setValue` not to propagate down to `setNativeProps`, otherwise it may try to access `setNativeProps` - // via component refs table that we override here. - c.refs = { - node: { - setNativeProps: jest.fn(), - }, - }; + expect(ref.current).not.toBeNull(); + jest.spyOn(ref.current, 'setNativeProps'); - anim.setValue(0.5); + opacity.setValue(0.5); - expect(nativeAnimatedModule.setAnimatedNodeValue).toBeCalledWith( + expect(NativeAnimatedModule.setAnimatedNodeValue).toBeCalledWith( expect.any(Number), 0.5, ); - expect(c.refs.node.setNativeProps).not.toHaveBeenCalled(); + expect(ref.current.setNativeProps).not.toHaveBeenCalled(); }); it('should set offset', () => { - const anim = new Animated.Value(0); - anim.setOffset(10); - anim.__makeNative(); - createAndMountComponent(Animated.View, { - style: { - opacity: anim, - }, - }); + const opacity = new Animated.Value(0); + opacity.setOffset(10); + opacity.__makeNative(); + + TestRenderer.create(); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'value', value: 0, offset: 10}, ); - anim.setOffset(20); - expect(nativeAnimatedModule.setAnimatedNodeOffset).toBeCalledWith( + opacity.setOffset(20); + expect(NativeAnimatedModule.setAnimatedNodeOffset).toBeCalledWith( expect.any(Number), 20, ); }); it('should flatten offset', () => { - const anim = new Animated.Value(0); - anim.__makeNative(); - createAndMountComponent(Animated.View, { - style: { - opacity: anim, - }, - }); + const opacity = new Animated.Value(0); + opacity.__makeNative(); + + TestRenderer.create(); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'value', value: 0, offset: 0}, ); - anim.flattenOffset(); - expect(nativeAnimatedModule.flattenAnimatedNodeOffset).toBeCalledWith( + opacity.flattenOffset(); + expect(NativeAnimatedModule.flattenAnimatedNodeOffset).toBeCalledWith( expect.any(Number), ); }); it('should extract offset', () => { - const anim = new Animated.Value(0); - anim.__makeNative(); - createAndMountComponent(Animated.View, { - style: { - opacity: anim, - }, - }); + const opacity = new Animated.Value(0); + opacity.__makeNative(); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + TestRenderer.create(); + + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'value', value: 0, offset: 0}, ); - anim.extractOffset(); - expect(nativeAnimatedModule.extractAnimatedNodeOffset).toBeCalledWith( + opacity.extractOffset(); + expect(NativeAnimatedModule.extractAnimatedNodeOffset).toBeCalledWith( expect.any(Number), ); }); @@ -169,7 +138,7 @@ describe('Native Animated', () => { const listener = jest.fn(); const id = value1.addListener(listener); expect( - nativeAnimatedModule.startListeningToAnimatedNodeValue, + NativeAnimatedModule.startListeningToAnimatedNodeValue, ).toHaveBeenCalledWith(value1.__getNativeTag()); NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', { @@ -190,7 +159,7 @@ describe('Native Animated', () => { value1.removeListener(id); expect( - nativeAnimatedModule.stopListeningToAnimatedNodeValue, + NativeAnimatedModule.stopListeningToAnimatedNodeValue, ).toHaveBeenCalledWith(value1.__getNativeTag()); NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', { @@ -207,7 +176,7 @@ describe('Native Animated', () => { const listener = jest.fn(); [1, 2, 3, 4].forEach(() => value1.addListener(listener)); expect( - nativeAnimatedModule.startListeningToAnimatedNodeValue, + NativeAnimatedModule.startListeningToAnimatedNodeValue, ).toHaveBeenCalledWith(value1.__getNativeTag()); NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', { @@ -219,7 +188,7 @@ describe('Native Animated', () => { value1.removeAllListeners(); expect( - nativeAnimatedModule.stopListeningToAnimatedNodeValue, + NativeAnimatedModule.stopListeningToAnimatedNodeValue, ).toHaveBeenCalledWith(value1.__getNativeTag()); NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', { @@ -237,8 +206,9 @@ describe('Native Animated', () => { const event = Animated.event([{nativeEvent: {state: {foo: value}}}], { useNativeDriver: true, }); - const c = createAndMountComponent(Animated.View, {onTouchMove: event}); - expect(nativeAnimatedModule.addAnimatedEventToView).toBeCalledWith( + + const root = TestRenderer.create(); + expect(NativeAnimatedModule.addAnimatedEventToView).toBeCalledWith( expect.any(Number), 'onTouchMove', { @@ -247,8 +217,11 @@ describe('Native Animated', () => { }, ); - c.componentWillUnmount(); - expect(nativeAnimatedModule.removeAnimatedEventFromView).toBeCalledWith( + expect( + NativeAnimatedModule.removeAnimatedEventFromView, + ).not.toHaveBeenCalled(); + root.unmount(); + expect(NativeAnimatedModule.removeAnimatedEventFromView).toBeCalledWith( expect.any(Number), 'onTouchMove', value.__getNativeTag(), @@ -261,10 +234,20 @@ describe('Native Animated', () => { const event = Animated.event([{notNativeEvent: {foo: value}}], { useNativeDriver: true, }); - expect(() => - createAndMountComponent(Animated.View, {onTouchMove: event}), - ).toThrowError(/nativeEvent/); - expect(nativeAnimatedModule.addAnimatedEventToView).not.toBeCalled(); + + jest.spyOn(console, 'error').mockImplementationOnce((...args) => { + if (args[0].startsWith('The above error occurred in the')) { + return; + } + console.errorDebug(...args); + }); + + expect(() => { + TestRenderer.create(); + }).toThrowError(/nativeEvent/); + expect(NativeAnimatedModule.addAnimatedEventToView).not.toBeCalled(); + + console.error.mockRestore(); }); it('should call listeners', () => { @@ -284,27 +267,21 @@ describe('Native Animated', () => { describe('Animated Graph', () => { it('creates and detaches nodes', () => { - const anim = new Animated.Value(0); - const c = createAndMountComponent(Animated.View, { - style: { - opacity: anim, - }, - }); + const opacity = new Animated.Value(0); + const root = TestRenderer.create(); - Animated.timing(anim, { + Animated.timing(opacity, { toValue: 10, duration: 1000, useNativeDriver: true, }).start(); - c.componentWillUnmount(); - - expect(nativeAnimatedModule.createAnimatedNode).toHaveBeenCalledTimes(3); - expect(nativeAnimatedModule.connectAnimatedNodes).toHaveBeenCalledTimes( + expect(NativeAnimatedModule.createAnimatedNode).toHaveBeenCalledTimes(3); + expect(NativeAnimatedModule.connectAnimatedNodes).toHaveBeenCalledTimes( 2, ); - expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), { @@ -317,34 +294,37 @@ describe('Native Animated', () => { ); expect( - nativeAnimatedModule.disconnectAnimatedNodes, + NativeAnimatedModule.disconnectAnimatedNodes, + ).not.toHaveBeenCalled(); + expect(NativeAnimatedModule.dropAnimatedNode).not.toHaveBeenCalled(); + + root.unmount(); + + expect( + NativeAnimatedModule.disconnectAnimatedNodes, ).toHaveBeenCalledTimes(2); - expect(nativeAnimatedModule.dropAnimatedNode).toHaveBeenCalledTimes(3); + expect(NativeAnimatedModule.dropAnimatedNode).toHaveBeenCalledTimes(3); }); it('sends a valid description for value, style and props nodes', () => { - const anim = new Animated.Value(0); - createAndMountComponent(Animated.View, { - style: { - opacity: anim, - }, - }); + const opacity = new Animated.Value(0); + TestRenderer.create(); - Animated.timing(anim, { + Animated.timing(opacity, { toValue: 10, duration: 1000, useNativeDriver: true, }).start(); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'value', value: 0, offset: 0}, ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'style', style: {opacity: expect.any(Number)}}, ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'props', props: {style: expect.any(Number)}}, ); @@ -356,31 +336,29 @@ describe('Native Animated', () => { first.__makeNative(); second.__makeNative(); - createAndMountComponent(Animated.View, { - style: { - opacity: Animated.add(first, second), - }, - }); + TestRenderer.create( + , + ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'addition', input: expect.any(Array)}, ); - const additionCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter( + const additionCalls = NativeAnimatedModule.createAnimatedNode.mock.calls.filter( call => call[1].type === 'addition', ); expect(additionCalls.length).toBe(1); const additionCall = additionCalls[0]; const additionNodeTag = additionCall[0]; - const additionConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( + const additionConnectionCalls = NativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( call => call[1] === additionNodeTag, ); expect(additionConnectionCalls.length).toBe(2); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( additionCall[1].input[0], {type: 'value', value: 1, offset: 0}, ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( additionCall[1].input[1], {type: 'value', value: 2, offset: 0}, ); @@ -392,31 +370,29 @@ describe('Native Animated', () => { first.__makeNative(); second.__makeNative(); - createAndMountComponent(Animated.View, { - style: { - opacity: Animated.subtract(first, second), - }, - }); + TestRenderer.create( + , + ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'subtraction', input: expect.any(Array)}, ); - const subtractionCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter( + const subtractionCalls = NativeAnimatedModule.createAnimatedNode.mock.calls.filter( call => call[1].type === 'subtraction', ); expect(subtractionCalls.length).toBe(1); const subtractionCall = subtractionCalls[0]; const subtractionNodeTag = subtractionCall[0]; - const subtractionConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( + const subtractionConnectionCalls = NativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( call => call[1] === subtractionNodeTag, ); expect(subtractionConnectionCalls.length).toBe(2); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( subtractionCall[1].input[0], {type: 'value', value: 2, offset: 0}, ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( subtractionCall[1].input[1], {type: 'value', value: 1, offset: 0}, ); @@ -428,31 +404,29 @@ describe('Native Animated', () => { first.__makeNative(); second.__makeNative(); - createAndMountComponent(Animated.View, { - style: { - opacity: Animated.multiply(first, second), - }, - }); + TestRenderer.create( + , + ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'multiplication', input: expect.any(Array)}, ); - const multiplicationCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter( + const multiplicationCalls = NativeAnimatedModule.createAnimatedNode.mock.calls.filter( call => call[1].type === 'multiplication', ); expect(multiplicationCalls.length).toBe(1); const multiplicationCall = multiplicationCalls[0]; const multiplicationNodeTag = multiplicationCall[0]; - const multiplicationConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( + const multiplicationConnectionCalls = NativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( call => call[1] === multiplicationNodeTag, ); expect(multiplicationConnectionCalls.length).toBe(2); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( multiplicationCall[1].input[0], {type: 'value', value: 2, offset: 0}, ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( multiplicationCall[1].input[1], {type: 'value', value: 1, offset: 0}, ); @@ -464,31 +438,29 @@ describe('Native Animated', () => { first.__makeNative(); second.__makeNative(); - createAndMountComponent(Animated.View, { - style: { - opacity: Animated.divide(first, second), - }, - }); + TestRenderer.create( + , + ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'division', input: expect.any(Array)}, ); - const divisionCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter( + const divisionCalls = NativeAnimatedModule.createAnimatedNode.mock.calls.filter( call => call[1].type === 'division', ); expect(divisionCalls.length).toBe(1); const divisionCall = divisionCalls[0]; const divisionNodeTag = divisionCall[0]; - const divisionConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( + const divisionConnectionCalls = NativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( call => call[1] === divisionNodeTag, ); expect(divisionConnectionCalls.length).toBe(2); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( divisionCall[1].input[0], {type: 'value', value: 4, offset: 0}, ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( divisionCall[1].input[1], {type: 'value', value: 2, offset: 0}, ); @@ -498,27 +470,25 @@ describe('Native Animated', () => { const value = new Animated.Value(4); value.__makeNative(); - createAndMountComponent(Animated.View, { - style: { - opacity: Animated.modulo(value, 4), - }, - }); + TestRenderer.create( + , + ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'modulus', modulus: 4, input: expect.any(Number)}, ); - const moduloCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter( + const moduloCalls = NativeAnimatedModule.createAnimatedNode.mock.calls.filter( call => call[1].type === 'modulus', ); expect(moduloCalls.length).toBe(1); const moduloCall = moduloCalls[0]; const moduloNodeTag = moduloCall[0]; - const moduloConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( + const moduloConnectionCalls = NativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( call => call[1] === moduloNodeTag, ); expect(moduloConnectionCalls.length).toBe(1); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( moduloCall[1].input, {type: 'value', value: 4, offset: 0}, ); @@ -528,20 +498,22 @@ describe('Native Animated', () => { const value = new Animated.Value(10); value.__makeNative(); - createAndMountComponent(Animated.View, { - style: { - opacity: value.interpolate({ - inputRange: [10, 20], - outputRange: [0, 1], - }), - }, - }); + TestRenderer.create( + , + ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'value', value: 10, offset: 0}, ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), { type: 'interpolation', @@ -551,29 +523,27 @@ describe('Native Animated', () => { extrapolateRight: 'extend', }, ); - const interpolationNodeTag = nativeAnimatedModule.createAnimatedNode.mock.calls.find( + const interpolationNodeTag = NativeAnimatedModule.createAnimatedNode.mock.calls.find( call => call[1].type === 'interpolation', )[0]; - const valueNodeTag = nativeAnimatedModule.createAnimatedNode.mock.calls.find( + const valueNodeTag = NativeAnimatedModule.createAnimatedNode.mock.calls.find( call => call[1].type === 'value', )[0]; - expect(nativeAnimatedModule.connectAnimatedNodes).toBeCalledWith( + expect(NativeAnimatedModule.connectAnimatedNodes).toBeCalledWith( valueNodeTag, interpolationNodeTag, ); }); it('sends a valid graph description for transform nodes', () => { - const value = new Animated.Value(0); - value.__makeNative(); + const translateX = new Animated.Value(0); + translateX.__makeNative(); - createAndMountComponent(Animated.View, { - style: { - transform: [{translateX: value}, {scale: 2}], - }, - }); + TestRenderer.create( + , + ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), { type: 'transform', @@ -597,86 +567,86 @@ describe('Native Animated', () => { const value = new Animated.Value(2); value.__makeNative(); - createAndMountComponent(Animated.View, { - style: { - opacity: Animated.diffClamp(value, 0, 20), - }, - }); + TestRenderer.create( + , + ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'diffclamp', input: expect.any(Number), max: 20, min: 0}, ); - const diffClampCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter( + const diffClampCalls = NativeAnimatedModule.createAnimatedNode.mock.calls.filter( call => call[1].type === 'diffclamp', ); expect(diffClampCalls.length).toBe(1); const diffClampCall = diffClampCalls[0]; const diffClampNodeTag = diffClampCall[0]; - const diffClampConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( + const diffClampConnectionCalls = NativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( call => call[1] === diffClampNodeTag, ); expect(diffClampConnectionCalls.length).toBe(1); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( diffClampCall[1].input, {type: 'value', value: 2, offset: 0}, ); }); it("doesn't call into native API if useNativeDriver is set to false", () => { - const anim = new Animated.Value(0); + const opacity = new Animated.Value(0); - const c = createAndMountComponent(Animated.View, { - style: { - opacity: anim, - }, - }); + const root = TestRenderer.create(); - Animated.timing(anim, { + Animated.timing(opacity, { toValue: 10, duration: 1000, useNativeDriver: false, }).start(); - c.componentWillUnmount(); + root.unmount(); - expect(nativeAnimatedModule.createAnimatedNode).not.toBeCalled(); + expect(NativeAnimatedModule.createAnimatedNode).not.toBeCalled(); }); it('fails when trying to run non-native animation on native node', () => { - const anim = new Animated.Value(0); + const opacity = new Animated.Value(0); + const ref = React.createRef(null); - createAndMountComponent(Animated.View, { - style: { - opacity: anim, - }, - }); + TestRenderer.create(); - Animated.timing(anim, { + // Necessary to simulate the native animation. + expect(ref.current).not.toBeNull(); + ref.current.setNativeProps = jest.fn(); + + Animated.timing(opacity, { toValue: 10, duration: 50, useNativeDriver: true, }).start(); jest.runAllTimers(); - Animated.timing(anim, { + Animated.timing(opacity, { toValue: 4, duration: 500, useNativeDriver: false, }).start(); - expect(jest.runAllTimers).toThrow(); + try { + process.env.NODE_ENV = 'development'; + expect(jest.runAllTimers).toThrow( + 'Attempting to run JS driven animation on animated node that has ' + + 'been moved to "native" earlier by starting an animation with ' + + '`useNativeDriver: true`', + ); + } finally { + process.env.NODE_ENV = 'test'; + } }); it('fails for unsupported styles', () => { - const anim = new Animated.Value(0); + const left = new Animated.Value(0); - createAndMountComponent(Animated.View, { - style: { - left: anim, - }, - }); + TestRenderer.create(); - const animation = Animated.timing(anim, { + const animation = Animated.timing(left, { toValue: 10, duration: 50, useNativeDriver: true, @@ -686,23 +656,21 @@ describe('Native Animated', () => { it('works for any `static` props and styles', () => { // Passing "unsupported" props should work just fine as long as they are not animated - const value = new Animated.Value(0); - value.__makeNative(); + const opacity = new Animated.Value(0); + opacity.__makeNative(); - createAndMountComponent(Animated.View, { - style: { - left: 10, - top: 20, - opacity: value, - }, - removeClippedSubviews: true, - }); + TestRenderer.create( + , + ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'style', style: {opacity: expect.any(Number)}}, ); - expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'props', props: {style: expect.any(Number)}}, ); @@ -718,7 +686,7 @@ describe('Native Animated', () => { useNativeDriver: true, }).start(); - expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), { @@ -739,7 +707,7 @@ describe('Native Animated', () => { tension: 164, useNativeDriver: true, }).start(); - expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), { @@ -764,7 +732,7 @@ describe('Native Animated', () => { mass: 3, useNativeDriver: true, }).start(); - expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), { @@ -788,7 +756,7 @@ describe('Native Animated', () => { speed: 10, useNativeDriver: true, }).start(); - expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), { @@ -815,7 +783,7 @@ describe('Native Animated', () => { useNativeDriver: true, }).start(); - expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), {type: 'decay', deceleration: 0.1, velocity: 10, iterations: 1}, @@ -834,7 +802,7 @@ describe('Native Animated', () => { {iterations: 10}, ).start(); - expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), {type: 'decay', deceleration: 0.1, velocity: 10, iterations: 10}, @@ -851,7 +819,7 @@ describe('Native Animated', () => { }); animation.start(); - expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), { @@ -863,10 +831,10 @@ describe('Native Animated', () => { expect.any(Function), ); const animationId = - nativeAnimatedModule.startAnimatingNode.mock.calls[0][0]; + NativeAnimatedModule.startAnimatingNode.mock.calls[0][0]; animation.stop(); - expect(nativeAnimatedModule.stopAnimation).toBeCalledWith(animationId); + expect(NativeAnimatedModule.stopAnimation).toBeCalledWith(animationId); }); }); }); diff --git a/Libraries/Animated/src/__tests__/__snapshots__/Animated-test.js.snap b/Libraries/Animated/src/__tests__/__snapshots__/Animated-test.js.snap deleted file mode 100644 index 721292188b39ec..00000000000000 --- a/Libraries/Animated/src/__tests__/__snapshots__/Animated-test.js.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Animated tests Animated bypasses \`setNativeProps\` in test environments 1`] = ` - -`; - -exports[`Animated tests Animated bypasses \`setNativeProps\` in test environments 2`] = ` - -`; diff --git a/Libraries/Animated/src/createAnimatedComponent.js b/Libraries/Animated/src/createAnimatedComponent.js index 8401f63c806775..4c0dc34c0f459a 100644 --- a/Libraries/Animated/src/createAnimatedComponent.js +++ b/Libraries/Animated/src/createAnimatedComponent.js @@ -15,6 +15,7 @@ const AnimatedProps = require('./nodes/AnimatedProps'); const React = require('react'); const invariant = require('invariant'); +const setAndForwardRef = require('../../Utilities/setAndForwardRef'); export type AnimatedComponentType = React.AbstractComponent< any, @@ -116,20 +117,26 @@ function createAnimatedComponent( oldPropsAnimated && oldPropsAnimated.__detach(); } - _setComponentRef = c => { - this._prevComponent = this._component; - this._component = c; - }; - - // A third party library can use getNode() - // to get the node reference of the decorated component - getNode() { - return this._component; - } - - setNativeProps(props) { - this._component.setNativeProps(props); - } + _setComponentRef = setAndForwardRef({ + getForwardedRef: () => this.props.forwardedRef, + setLocalRef: ref => { + this._prevComponent = this._component; + this._component = ref; + + // TODO: Delete this in a future release. + if (ref != null && ref.getNode == null) { + ref.getNode = () => { + console.warn( + '%s: Calling `getNode()` on the ref of an Animated component ' + + 'is no longer necessary. You can now directly use the ref ' + + 'instead. This method will be removed in a future release.', + ref.constructor.name ?? '<>', + ); + return ref; + }; + } + }, + }); render() { const props = this._propsAnimated.__getValue(); @@ -182,7 +189,14 @@ function createAnimatedComponent( } } - return AnimatedComponent; + return React.forwardRef(function AnimatedComponentWrapper(props, ref) { + return ( + + ); + }); } module.exports = createAnimatedComponent; diff --git a/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorSourceMapStatus-test.js.snap b/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorSourceMapStatus-test.js.snap index 1877e27ac7cbd8..dcb78ed5130464 100644 --- a/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorSourceMapStatus-test.js.snap +++ b/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorSourceMapStatus-test.js.snap @@ -27,7 +27,7 @@ exports[`LogBoxInspectorSourceMapStatus should render for failed 1`] = ` } } > - - { } } +function TouchableNativeMethodChecker< + T: React.AbstractComponent, +>(props: {|Component: T, name: string|}): React.Node { + const [status, setStatus] = useState(null); + const ref = useRef>(null); + + useEffect(() => { + setStatus(ref.current != null && typeof ref.current.measure === 'function'); + }, []); + + return ( + + + + + + {props.name + ': '} + {status == null + ? 'Missing Ref!' + : status === true + ? 'Native Methods Exist' + : 'Native Methods Missing!'} + + + ); +} + +function TouchableNativeMethods() { + return ( + + + + + ); +} + class TouchableDisabled extends React.Component<{}> { render() { return ( @@ -551,6 +595,13 @@ exports.examples = [ return ; }, }, + { + title: 'Touchable Native Methods', + description: ('Some components expose native methods like `measure`.': string), + render: function(): React.Element { + return ; + }, + }, { title: 'Disabled Touchable*', description: (' components accept disabled prop which prevents ' +