Skip to content

Commit 8fc6f1b

Browse files
authored
feat: Web StrictMode e2e test and playwright setup (#8715)
## Summary This PR adds playwright setup for web e2e tests. It also adds the example file for the fix implemented in #8567 and implements the first e2e test that checks if the example works properly. ## Comparison I compared test results for the working and non-working implementation and all of them work as expected. To change the current working implementation to non-working, you can simply remove the following lines of code from the `packages/react-native-reanimated/src/createAnimatedComponent/AnimatedComponent.tsx`: ```tsx while (dummyClone?.firstChild) { element.appendChild(dummyClone.firstChild); } delete element.dummyClone; ``` ### For incorrect implementation <img width="591" height="176" alt="Screenshot 2025-12-07 at 01 10 21" src="https://github.com/user-attachments/assets/aee28362-5df3-4198-8f70-1278f1fb9392" /> ### For correct implementation <img width="526" height="148" alt="Screenshot 2025-12-07 at 01 28 38" src="https://github.com/user-attachments/assets/b794be89-c6a7-45d7-ac95-d889a03f97b6" />
1 parent 51904bb commit 8fc6f1b

File tree

8 files changed

+290
-2
lines changed

8 files changed

+290
-2
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,8 @@ CompilationDatabase
7575
compile_commands.json
7676

7777
build/
78+
79+
# Playwright
80+
test-results/
81+
playwright-report/
82+
playwright/.cache/
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { useEffect, useState, StrictMode } from 'react';
2+
import { StyleSheet, View, Text, Button } from 'react-native';
3+
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
4+
5+
export default function LayoutAnimationsStrictMode() {
6+
return (
7+
<StrictComparison title="Strict Mode vs Non-Strict Mode">
8+
<LayoutAnimations />
9+
</StrictComparison>
10+
);
11+
}
12+
13+
function LayoutAnimations() {
14+
const [hide, setHide] = useState(true);
15+
16+
useEffect(() => {
17+
const timer = setTimeout(() => setHide(false), 200);
18+
return () => clearTimeout(timer);
19+
}, []);
20+
21+
return (
22+
<View style={styles.container}>
23+
<Button
24+
title={hide ? 'Show' : 'Hide'}
25+
onPress={() => setHide((prev) => !prev)}
26+
/>
27+
28+
{hide ? null : (
29+
<>
30+
<Text>Entering</Text>
31+
<Animated.View entering={FadeIn}>
32+
<Square />
33+
</Animated.View>
34+
35+
<Text>Entering and exiting</Text>
36+
<Animated.View entering={FadeIn} exiting={FadeOut}>
37+
<Square />
38+
</Animated.View>
39+
40+
<Text>Exiting</Text>
41+
<Animated.View exiting={FadeOut}>
42+
<Square />
43+
</Animated.View>
44+
</>
45+
)}
46+
</View>
47+
);
48+
}
49+
50+
function StrictComparison({
51+
children,
52+
title,
53+
}: {
54+
children: React.ReactNode;
55+
title: string;
56+
}) {
57+
return (
58+
<View style={styles.wrapper}>
59+
<Text style={styles.title}>{title}</Text>
60+
<Text style={styles.descriptionText}>
61+
This example demonstrates layout animations in React&apos;s Strict Mode
62+
vs Non-Strict Mode. Both columns should behave identically - animations
63+
should work the same regardless of Strict Mode.
64+
</Text>
65+
66+
<View style={styles.columnsContainer}>
67+
<View style={styles.column} testID="strict-mode-column">
68+
<Text>Strict mode</Text>
69+
<StrictMode>{children}</StrictMode>
70+
</View>
71+
<View style={styles.column} testID="non-strict-column">
72+
<Text>Non-strict</Text>
73+
{children}
74+
</View>
75+
</View>
76+
</View>
77+
);
78+
}
79+
80+
function Square() {
81+
return <View style={styles.square} testID="box" />;
82+
}
83+
84+
const styles = StyleSheet.create({
85+
wrapper: {
86+
gap: 12,
87+
alignItems: 'center',
88+
flex: 1,
89+
padding: 16,
90+
},
91+
title: {
92+
fontSize: 20,
93+
alignSelf: 'center',
94+
},
95+
descriptionText: {
96+
fontSize: 14,
97+
textAlign: 'center',
98+
color: '#666',
99+
paddingHorizontal: 8,
100+
},
101+
columnsContainer: {
102+
flex: 1,
103+
flexDirection: 'row',
104+
justifyContent: 'center',
105+
gap: 20,
106+
},
107+
column: {
108+
flex: 1,
109+
alignItems: 'center',
110+
gap: 8,
111+
},
112+
container: {
113+
flex: 1,
114+
gap: 20,
115+
width: 200,
116+
backgroundColor: '#fff',
117+
alignItems: 'center',
118+
padding: 12,
119+
},
120+
square: {
121+
width: 50,
122+
height: 50,
123+
borderRadius: 10,
124+
backgroundColor: 'red',
125+
},
126+
});

apps/common-app/src/apps/reanimated/examples/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import DragAndSnapExample from './DragAndSnapExample';
3434
import DynamicColorIOSExample from './DynamicColorIOSExample';
3535
import PlatformColorExample from './PlatformColorExample';
3636
import EmojiWaterfallExample from './EmojiWaterfallExample';
37-
import EmptyExample from './EmptyExample';
37+
import StrictModeComparison from './LayoutAnimations/StrictModeComparisonExample';
3838
import ExtrapolationExample from './ExtrapolationExample';
3939
import FilterExample from './FilterExample';
4040
import FlatListWithLayoutAnimations from './FlatListWithLayoutAnimationsExample';
@@ -160,6 +160,7 @@ import WorkletFactoryCrash from './WorkletFactoryCrashExample';
160160
import WorkletRuntimeExample from './WorkletRuntimeExample';
161161
import InstanceDiscoveryExample from './InstanceDiscoveryExample';
162162
import ShadowNodesCloningExample from './ShadowNodesCloningExample';
163+
import EmptyExample from './EmptyExample';
163164

164165
export const REAPlatform = {
165166
IOS: 'ios',
@@ -830,6 +831,10 @@ export const EXAMPLES: Record<string, Example> = {
830831
title: '[LA] Mounting Unmounting',
831832
screen: MountingUnmounting,
832833
},
834+
StrictModeComparison: {
835+
title: '[LA] Strict Mode Comparison',
836+
screen: StrictModeComparison,
837+
},
833838
ReactionsCounterExample: {
834839
title: '[LA] Reactions counter',
835840
screen: ReactionsCounterExample,

apps/web-example/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ web-build/
1313
# macOS
1414
.DS_Store
1515

16+
# Playwright
17+
test-results/
18+
playwright-report/
19+
playwright/.cache/
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { expect, type Page, test } from '@playwright/test';
2+
3+
// Wait time for exit animations to complete before checking box count
4+
const ANIMATION_WAIT_TIME = 500;
5+
6+
const getColumnButton = (page: Page, label: string) =>
7+
page
8+
.getByTestId(
9+
label === 'Strict mode' ? 'strict-mode-column' : 'non-strict-column'
10+
)
11+
.getByRole('button')
12+
.filter({ hasText: /^(Show|Hide)$/i });
13+
14+
const countBoxes = async (page: Page) => await page.getByTestId('box').count();
15+
16+
const waitForBoxCount = async (
17+
page: Page,
18+
expectedCount: number,
19+
timeout = 2000
20+
) => {
21+
// Wait for exit animations to complete before checking
22+
await page.waitForTimeout(ANIMATION_WAIT_TIME);
23+
await expect(async () => {
24+
expect(await countBoxes(page)).toBe(expectedCount);
25+
}).toPass({ timeout: timeout - ANIMATION_WAIT_TIME });
26+
};
27+
28+
test.describe('StrictModeComparison', () => {
29+
test.beforeEach(async ({ page }) => {
30+
await page.goto('/Reanimated/StrictModeComparison');
31+
await page.waitForSelector('text=Strict Mode vs Non-Strict Mode');
32+
33+
// Wait for the state change to make the boxes visible
34+
await page.waitForTimeout(ANIMATION_WAIT_TIME);
35+
});
36+
37+
test('displays 6 red boxes initially', async ({ page }) => {
38+
await waitForBoxCount(page, 6);
39+
});
40+
41+
test('hides and shows boxes in strict mode column', async ({ page }) => {
42+
const button = getColumnButton(page, 'Strict mode');
43+
44+
await button.click();
45+
await waitForBoxCount(page, 3);
46+
47+
await button.click();
48+
await waitForBoxCount(page, 6);
49+
});
50+
51+
test('hides and shows boxes in non-strict mode column', async ({ page }) => {
52+
const button = getColumnButton(page, 'Non-strict');
53+
54+
await button.click();
55+
await waitForBoxCount(page, 3);
56+
57+
await button.click();
58+
await waitForBoxCount(page, 6);
59+
});
60+
61+
test('toggles both columns independently', async ({ page }) => {
62+
await getColumnButton(page, 'Strict mode').click();
63+
await waitForBoxCount(page, 3);
64+
65+
await getColumnButton(page, 'Non-strict').click();
66+
await waitForBoxCount(page, 0, 3000);
67+
68+
await getColumnButton(page, 'Strict mode').click();
69+
await waitForBoxCount(page, 3);
70+
71+
await getColumnButton(page, 'Non-strict').click();
72+
await waitForBoxCount(page, 6);
73+
});
74+
});

apps/web-example/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
"format": "prettier --write --list-different .",
1111
"build": "",
1212
"lint": "eslint . --max-warnings 0",
13-
"type:check": "tsc --noEmit"
13+
"type:check": "tsc --noEmit",
14+
"test:e2e": "playwright test",
15+
"test:e2e:ui": "playwright test --ui",
16+
"test:e2e:headed": "playwright test --headed"
1417
},
1518
"dependencies": {
1619
"common-app": "workspace:*",
@@ -23,6 +26,7 @@
2326
},
2427
"devDependencies": {
2528
"@expo/metro-runtime": "6.1.2",
29+
"@playwright/test": "1.57.0",
2630
"@types/react": "19.2.2",
2731
"eslint": "9.37.0",
2832
"prettier": "3.6.2",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { defineConfig } from '@playwright/test';
2+
3+
export default defineConfig({
4+
testDir: './e2e',
5+
use: {
6+
baseURL: 'http://localhost:8081',
7+
},
8+
webServer: {
9+
command: 'yarn start',
10+
url: 'http://localhost:8081',
11+
reuseExistingServer: true,
12+
},
13+
// Retry failed tests once in CI
14+
retries: process.env.CI ? 1 : 0,
15+
});

yarn.lock

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6757,6 +6757,17 @@ __metadata:
67576757
languageName: node
67586758
linkType: hard
67596759

6760+
"@playwright/test@npm:1.57.0":
6761+
version: 1.57.0
6762+
resolution: "@playwright/test@npm:1.57.0"
6763+
dependencies:
6764+
playwright: "npm:1.57.0"
6765+
bin:
6766+
playwright: cli.js
6767+
checksum: 10/07f5ba4841b2db1dea70d821004c5156b692488e13523c096ce3487d30f95f34ccf30ba6467ece60c86faac27ae382213b7eacab48a695550981b2e811e5e579
6768+
languageName: node
6769+
linkType: hard
6770+
67606771
"@pnpm/config.env-replace@npm:^1.1.0":
67616772
version: 1.1.0
67626773
resolution: "@pnpm/config.env-replace@npm:1.1.0"
@@ -18082,6 +18093,16 @@ __metadata:
1808218093
languageName: node
1808318094
linkType: hard
1808418095

18096+
"fsevents@npm:2.3.2":
18097+
version: 2.3.2
18098+
resolution: "fsevents@npm:2.3.2"
18099+
dependencies:
18100+
node-gyp: "npm:latest"
18101+
checksum: 10/6b5b6f5692372446ff81cf9501c76e3e0459a4852b3b5f1fc72c103198c125a6b8c72f5f166bdd76ffb2fca261e7f6ee5565daf80dca6e571e55bcc589cc1256
18102+
conditions: os=darwin
18103+
languageName: node
18104+
linkType: hard
18105+
1808518106
"fsevents@npm:^2.3.2, fsevents@npm:^2.3.3, fsevents@npm:~2.3.2":
1808618107
version: 2.3.3
1808718108
resolution: "fsevents@npm:2.3.3"
@@ -18092,6 +18113,15 @@ __metadata:
1809218113
languageName: node
1809318114
linkType: hard
1809418115

18116+
"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>":
18117+
version: 2.3.2
18118+
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
18119+
dependencies:
18120+
node-gyp: "npm:latest"
18121+
conditions: os=darwin
18122+
languageName: node
18123+
linkType: hard
18124+
1809518125
"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A^2.3.3#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>":
1809618126
version: 2.3.3
1809718127
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
@@ -26583,6 +26613,30 @@ __metadata:
2658326613
languageName: node
2658426614
linkType: hard
2658526615

26616+
"playwright-core@npm:1.57.0":
26617+
version: 1.57.0
26618+
resolution: "playwright-core@npm:1.57.0"
26619+
bin:
26620+
playwright-core: cli.js
26621+
checksum: 10/ec066602f0196f036006caee14a30d0a57533a76673bb9a0c609ef56e21decf018f0e8d402ba2fb18251393be6a1c9e193c83266f1670fe50838c5340e220de0
26622+
languageName: node
26623+
linkType: hard
26624+
26625+
"playwright@npm:1.57.0":
26626+
version: 1.57.0
26627+
resolution: "playwright@npm:1.57.0"
26628+
dependencies:
26629+
fsevents: "npm:2.3.2"
26630+
playwright-core: "npm:1.57.0"
26631+
dependenciesMeta:
26632+
fsevents:
26633+
optional: true
26634+
bin:
26635+
playwright: cli.js
26636+
checksum: 10/241559210f98ef11b6bd6413f2d29da7ef67c7865b72053192f0d164fab9e0d3bd47913b3351d5de6433a8aff2d8424d4b8bd668df420bf4dda7ae9fcd37b942
26637+
languageName: node
26638+
linkType: hard
26639+
2658626640
"plist@npm:^3.0.5":
2658726641
version: 3.1.0
2658826642
resolution: "plist@npm:3.1.0"
@@ -33139,6 +33193,7 @@ __metadata:
3313933193
resolution: "web-example@workspace:apps/web-example"
3314033194
dependencies:
3314133195
"@expo/metro-runtime": "npm:6.1.2"
33196+
"@playwright/test": "npm:1.57.0"
3314233197
"@types/react": "npm:19.2.2"
3314333198
common-app: "workspace:*"
3314433199
eslint: "npm:9.37.0"

0 commit comments

Comments
 (0)