diff --git a/packages/react-native-reanimated/__tests__/hooks.useAnimatedStyle.test.tsx b/packages/react-native-reanimated/__tests__/hooks.useAnimatedStyle.test.tsx
new file mode 100644
index 00000000000..edb18da0e2c
--- /dev/null
+++ b/packages/react-native-reanimated/__tests__/hooks.useAnimatedStyle.test.tsx
@@ -0,0 +1,91 @@
+import { Button, View } from 'react-native';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+import Animated, {
+ useSharedValue,
+ getAnimatedStyle,
+ useAnimatedStyle,
+ withTiming,
+} from '../src';
+
+jest.useFakeTimers();
+
+describe('Tests of inline styles', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
+ test('useAnimatedStyle', () => {
+ function UseAnimatedStyle() {
+ const width = useSharedValue(100);
+
+ const handlePress = () => {
+ width.value = width.value + 50;
+ };
+
+ const animatedStyle = useAnimatedStyle(() => {
+ return {
+ width: width.value,
+ };
+ }, [width]);
+
+ return (
+
+
+
+
+ );
+ }
+
+ render();
+ const view = screen.getByTestId('view');
+ const button = screen.getByTestId('button');
+
+ expect(getAnimatedStyle(view)).toEqual({ width: 100 });
+
+ fireEvent.press(button);
+
+ jest.runAllTimers();
+
+ expect(getAnimatedStyle(view)).toEqual({ width: 150 });
+ });
+
+ test('useAnimatedStyle with withTiming', () => {
+ function UseAnimatedStyle() {
+ const width = useSharedValue(100);
+
+ const handlePress = () => {
+ width.value = withTiming(width.value + 50, { duration: 500 });
+ };
+
+ const animatedStyle = useAnimatedStyle(() => {
+ return {
+ width: width.value,
+ };
+ }, [width]);
+
+ return (
+
+
+
+
+ );
+ }
+
+ render();
+ const view = screen.getByTestId('view');
+ const button = screen.getByTestId('button');
+
+ expect(getAnimatedStyle(view)).toEqual({ width: 100 });
+
+ fireEvent.press(button);
+
+ jest.runAllTimers();
+
+ expect(getAnimatedStyle(view)).toEqual({ width: 150 });
+ });
+});
diff --git a/packages/react-native-reanimated/__tests__/inlineStyles.test.tsx b/packages/react-native-reanimated/__tests__/inlineStyles.test.tsx
new file mode 100644
index 00000000000..3f84de7bfcc
--- /dev/null
+++ b/packages/react-native-reanimated/__tests__/inlineStyles.test.tsx
@@ -0,0 +1,197 @@
+import { Button, View } from 'react-native';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+import Animated, {
+ useSharedValue,
+ getAnimatedStyle,
+ useAnimatedStyle,
+ withTiming,
+} from '../src';
+
+jest.useFakeTimers();
+
+describe('Tests of inline styles', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
+ test('Inline styles', () => {
+ function InlineStyle() {
+ const width = useSharedValue(100);
+
+ const handlePress = () => {
+ width.value = width.value + 50;
+ };
+
+ return (
+
+
+
+
+ );
+ }
+
+ render();
+ const view = screen.getByTestId('view');
+ const button = screen.getByTestId('button');
+
+ expect(getAnimatedStyle(view)).toEqual({ width: 100 });
+
+ fireEvent.press(button);
+ jest.runAllTimers();
+
+ expect(view).toHaveAnimatedStyle({ width: 150 });
+ });
+
+ test('Inline styles with withTiming', () => {
+ function InlineStyle() {
+ const width = useSharedValue(100);
+
+ const handlePress = () => {
+ width.value = withTiming(width.value + 50, { duration: 500 });
+ };
+
+ return (
+
+
+
+
+ );
+ }
+
+ render();
+ const view = screen.getByTestId('view');
+ const button = screen.getByTestId('button');
+
+ expect(getAnimatedStyle(view)).toEqual({
+ width: 100,
+ height: 100,
+ backgroundColor: 'violet',
+ });
+
+ fireEvent.press(button);
+
+ jest.runAllTimers();
+
+ expect(getAnimatedStyle(view)).toEqual({
+ width: 150,
+ height: 100,
+ backgroundColor: 'violet',
+ });
+ });
+
+ test('Double inline styles (single object)', () => {
+ function UseAnimatedStyle() {
+ const width = useSharedValue(100);
+ const height = useSharedValue(100);
+
+ const handlePress = () => {
+ width.value = width.value + 50;
+ height.value = height.value + 50;
+ };
+
+ return (
+
+
+
+
+ );
+ }
+
+ render();
+ const view = screen.getByTestId('view');
+ const button = screen.getByTestId('button');
+
+ expect(getAnimatedStyle(view)).toEqual({ width: 100, height: 100 });
+
+ fireEvent.press(button);
+
+ jest.runAllTimers();
+
+ expect(getAnimatedStyle(view)).toEqual({ width: 150, height: 150 });
+ });
+
+ test('Double inline styles (array)', () => {
+ function UseAnimatedStyle() {
+ const width = useSharedValue(100);
+ const height = useSharedValue(100);
+
+ const handlePress = () => {
+ width.value = width.value + 50;
+ height.value = height.value + 50;
+ };
+
+ return (
+
+
+
+
+ );
+ }
+
+ render();
+ const view = screen.getByTestId('view');
+ const button = screen.getByTestId('button');
+
+ expect(getAnimatedStyle(view)).toEqual({ width: 100, height: 100 });
+
+ fireEvent.press(button);
+
+ jest.runAllTimers();
+
+ expect(getAnimatedStyle(view)).toEqual({ width: 150, height: 150 });
+ });
+
+ test('Inline & useAnimatedStyle()', () => {
+ function UseAnimatedStyle() {
+ const width = useSharedValue(100);
+ const height = useSharedValue(100);
+
+ const handlePress = () => {
+ width.value = width.value + 50;
+ height.value = height.value + 50;
+ };
+
+ const animatedStyle = useAnimatedStyle(() => {
+ return {
+ width: width.value,
+ };
+ }, [width]);
+
+ return (
+
+
+
+
+ );
+ }
+
+ render();
+ const view = screen.getByTestId('view');
+ const button = screen.getByTestId('button');
+
+ expect(getAnimatedStyle(view)).toEqual({ width: 100, height: 100 });
+
+ fireEvent.press(button);
+
+ jest.runAllTimers();
+
+ expect(getAnimatedStyle(view)).toEqual({ width: 150, height: 150 });
+ });
+});
diff --git a/packages/react-native-reanimated/package.json b/packages/react-native-reanimated/package.json
index 765393335ff..c741a039ef5 100644
--- a/packages/react-native-reanimated/package.json
+++ b/packages/react-native-reanimated/package.json
@@ -106,7 +106,7 @@
"@react-native/typescript-config": "0.75.1",
"@testing-library/jest-native": "^4.0.4",
"@testing-library/react-hooks": "^8.0.0",
- "@testing-library/react-native": "^7.1.0",
+ "@testing-library/react-native": "^12.5.2",
"@types/babel__core": "^7.20.0",
"@types/babel__generator": "^7.6.4",
"@types/babel__traverse": "^7.14.2",
diff --git a/packages/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts b/packages/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts
index 110cd86d263..bdaffe7810d 100644
--- a/packages/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts
+++ b/packages/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts
@@ -105,6 +105,7 @@ export interface IAnimatedComponentInternal {
*/
_componentViewTag: number;
_isFirstRender: boolean;
+ jestInlineStyle: NestedArray | undefined;
jestAnimatedStyle: { value: StyleProps };
_component: AnimatedComponentRef | HTMLElement | null;
_sharedElementTransition: SharedTransition | null;
diff --git a/packages/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx b/packages/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
index b842ef97a16..bcc8c8d7604 100644
--- a/packages/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
+++ b/packages/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
@@ -31,6 +31,7 @@ import type {
IAnimatedComponentInternal,
ViewInfo,
INativeEventsManager,
+ NestedArray,
} from './commonTypes';
import { flattenArray } from './utils';
import setAndForwardRef from './setAndForwardRef';
@@ -53,6 +54,7 @@ import { NativeEventsManager } from './NativeEventsManager';
import type { ReanimatedHTMLElement } from '../js-reanimated';
const IS_WEB = isWeb();
+const IS_JEST = isJest();
if (IS_WEB) {
configureWebLayoutAnimations();
@@ -121,6 +123,7 @@ export function createAnimatedComponent(
_animatedProps?: Partial>;
_componentViewTag = -1;
_isFirstRender = true;
+ jestInlineStyle: NestedArray | undefined;
jestAnimatedStyle: { value: StyleProps } = { value: {} };
_component: AnimatedComponentRef | HTMLElement | null = null;
_sharedElementTransition: SharedTransition | null = null;
@@ -136,7 +139,7 @@ export function createAnimatedComponent(
constructor(props: AnimatedComponentProps) {
super(props);
- if (isJest()) {
+ if (IS_JEST) {
this.jestAnimatedStyle = { value: {} };
}
const entering = this.props.entering;
@@ -361,7 +364,7 @@ export function createAnimatedComponent(
name: viewName,
shadowNodeWrapper,
});
- if (isJest()) {
+ if (IS_JEST) {
/**
* We need to connect Jest's TestObject instance whose contains just props object
* with the updateProps() function where we update the properties of the component.
@@ -561,7 +564,7 @@ export function createAnimatedComponent(
render() {
const filteredProps = this._PropsFilter.filterNonAnimatedProps(this);
- if (isJest()) {
+ if (IS_JEST) {
filteredProps.jestAnimatedStyle = this.jestAnimatedStyle;
}
@@ -590,10 +593,18 @@ export function createAnimatedComponent(
const nativeID =
skipEntering || !isFabric() ? undefined : `${this.reanimatedID}`;
+ const jestProps = IS_JEST
+ ? {
+ jestInlineStyle: this.props.style,
+ jestAnimatedStyle: this.jestAnimatedStyle,
+ }
+ : {};
+
return (
void}
diff --git a/packages/react-native-reanimated/src/jestUtils.ts b/packages/react-native-reanimated/src/jestUtils.ts
index 5b221a1348e..3d31c3a9e5a 100644
--- a/packages/react-native-reanimated/src/jestUtils.ts
+++ b/packages/react-native-reanimated/src/jestUtils.ts
@@ -27,22 +27,75 @@ const defaultFramerateConfig = {
fps: 60,
};
+const isEmpty = (obj: object) => Object.keys(obj).length === 0;
+const getStylesFromObject = (obj: object) => {
+ return obj === undefined
+ ? {}
+ : Object.fromEntries(
+ Object.entries(obj).map(([property, value]) => [
+ property,
+ value._isReanimatedSharedValue ? value.value : value,
+ ])
+ );
+};
+
+type StyleValue = { value: unknown };
+type JestInlineStyle =
+ | {
+ [s: string]: StyleValue;
+ }
+ | ArrayLike;
+
const getCurrentStyle = (component: TestComponent): DefaultStyle => {
const styleObject = component.props.style;
+
let currentStyle = {};
+
if (Array.isArray(styleObject)) {
+ // It is possible that style may contain nested arrays. Currently, neither `StyleSheet.flatten` nor `flattenArray` solve this issue.
+ // Hence, we're not handling nested arrays at the moment - this is a known limitation of the current implementation.
styleObject.forEach((style) => {
currentStyle = {
...currentStyle,
...style,
};
});
- } else {
+
+ return currentStyle;
+ }
+
+ const jestInlineStyles = component.props.jestInlineStyle as JestInlineStyle;
+ const jestAnimatedStyleValue = component.props.jestAnimatedStyle?.value;
+
+ if (Array.isArray(jestInlineStyles)) {
+ for (const obj of jestInlineStyles) {
+ if ('jestAnimatedStyle' in obj) {
+ continue;
+ }
+
+ const inlineStyles = getStylesFromObject(obj);
+
+ currentStyle = {
+ ...currentStyle,
+ ...inlineStyles,
+ };
+ }
+
currentStyle = {
...styleObject,
- ...component.props.jestAnimatedStyle?.value,
+ ...currentStyle,
+ ...jestAnimatedStyleValue,
};
+
+ return currentStyle;
}
+
+ const inlineStyles = getStylesFromObject(jestInlineStyles);
+
+ currentStyle = isEmpty(jestAnimatedStyleValue as object)
+ ? { ...styleObject, ...inlineStyles }
+ : { ...styleObject, ...jestAnimatedStyleValue };
+
return currentStyle;
};
diff --git a/yarn.lock b/yarn.lock
index 519539de94c..33f695d372a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7086,16 +7086,22 @@ __metadata:
languageName: node
linkType: hard
-"@testing-library/react-native@npm:^7.1.0":
- version: 7.2.0
- resolution: "@testing-library/react-native@npm:7.2.0"
+"@testing-library/react-native@npm:^12.5.2":
+ version: 12.5.2
+ resolution: "@testing-library/react-native@npm:12.5.2"
dependencies:
- pretty-format: "npm:^26.0.1"
+ jest-matcher-utils: "npm:^29.7.0"
+ pretty-format: "npm:^29.7.0"
+ redent: "npm:^3.0.0"
peerDependencies:
- react: ">=16.0.0"
+ jest: ">=28.0.0"
+ react: ">=16.8.0"
react-native: ">=0.59"
- react-test-renderer: ">=16.0.0"
- checksum: 10/477146d65bbf699ce389f42e1935b0c42e091504ad9041fd97f0183a1dafd2e6f71337e75ab0b2f929a530bc1841b5d42d9ed019d89a928b23103ad407fc1c1a
+ react-test-renderer: ">=16.8.0"
+ peerDependenciesMeta:
+ jest:
+ optional: true
+ checksum: 10/8d7f41db709b47085642ab10de039e3e6018e5cdefbed78da040879d52815125f21be7a8032422370fad36b649c6fc831b16e97d9c6bfa3f97e5d20944d01fe2
languageName: node
linkType: hard
@@ -17880,7 +17886,7 @@ __metadata:
languageName: node
linkType: hard
-"pretty-format@npm:^26.0.1, pretty-format@npm:^26.5.2, pretty-format@npm:^26.6.2":
+"pretty-format@npm:^26.5.2, pretty-format@npm:^26.6.2":
version: 26.6.2
resolution: "pretty-format@npm:26.6.2"
dependencies:
@@ -18408,7 +18414,7 @@ __metadata:
"@react-native/typescript-config": "npm:0.75.1"
"@testing-library/jest-native": "npm:^4.0.4"
"@testing-library/react-hooks": "npm:^8.0.0"
- "@testing-library/react-native": "npm:^7.1.0"
+ "@testing-library/react-native": "npm:^12.5.2"
"@types/babel__core": "npm:^7.20.0"
"@types/babel__generator": "npm:^7.6.4"
"@types/babel__traverse": "npm:^7.14.2"