From cfbfc029b32ff9f0f05e72a7182d0f16e7deb814 Mon Sep 17 00:00:00 2001 From: Krzysztof Wojnar Date: Mon, 17 Nov 2025 20:10:14 +0100 Subject: [PATCH 1/8] chore(e2e): semver added, config formatted --- FabricExample/package.json | 2 ++ FabricExample/yarn.lock | 18 ++++++++++++++++++ scripts/e2e/android-devices.js | 11 +++++------ scripts/e2e/detox-utils.cjs | 6 +++--- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/FabricExample/package.json b/FabricExample/package.json index 91199b09f3..02660fa5c9 100644 --- a/FabricExample/package.json +++ b/FabricExample/package.json @@ -51,6 +51,7 @@ "@types/jest": "^29.5.13", "@types/react": "^19.1.1", "@types/react-test-renderer": "^19.1.0", + "@types/semver": "^7", "babel-jest": "^29.6.3", "detox": "^20.45.1", "eslint": "^8.19.0", @@ -58,6 +59,7 @@ "patch-package": "^8.0.0", "prettier": "2.8.8", "react-test-renderer": "19.1.1", + "semver": "^7.7.3", "ts-jest": "^29.0.3", "typescript": "5.0.4" }, diff --git a/FabricExample/yarn.lock b/FabricExample/yarn.lock index 80cff62243..32a5430ed2 100644 --- a/FabricExample/yarn.lock +++ b/FabricExample/yarn.lock @@ -3076,6 +3076,13 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7": + version: 7.7.1 + resolution: "@types/semver@npm:7.7.1" + checksum: 10c0/c938aef3bf79a73f0f3f6037c16e2e759ff40c54122ddf0b2583703393d8d3127130823facb880e694caa324eb6845628186aac1997ee8b31dc2d18fafe26268 + languageName: node + linkType: hard + "@types/stack-utils@npm:^2.0.0": version: 2.0.3 resolution: "@types/stack-utils@npm:2.0.3" @@ -3316,6 +3323,7 @@ __metadata: "@types/jest": "npm:^29.5.13" "@types/react": "npm:^19.1.1" "@types/react-test-renderer": "npm:^19.1.0" + "@types/semver": "npm:^7" babel-jest: "npm:^29.6.3" detox: "npm:^20.45.1" eslint: "npm:^8.19.0" @@ -3333,6 +3341,7 @@ __metadata: react-native-screens: "link:../" react-native-worklets: "npm:~0.6.0" react-test-renderer: "npm:19.1.1" + semver: "npm:^7.7.3" ts-jest: "npm:^29.0.3" typescript: "npm:5.0.4" languageName: unknown @@ -9464,6 +9473,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.7.3": + version: 7.7.3 + resolution: "semver@npm:7.7.3" + bin: + semver: bin/semver.js + checksum: 10c0/4afe5c986567db82f44c8c6faef8fe9df2a9b1d98098fc1721f57c696c4c21cebd572f297fc21002f81889492345b8470473bc6f4aff5fb032a6ea59ea2bc45e + languageName: node + linkType: hard + "send@npm:0.19.0": version: 0.19.0 resolution: "send@npm:0.19.0" diff --git a/scripts/e2e/android-devices.js b/scripts/e2e/android-devices.js index 1e773e1146..f6290a57ea 100644 --- a/scripts/e2e/android-devices.js +++ b/scripts/e2e/android-devices.js @@ -16,7 +16,7 @@ function resolveAttachedAndroidDeviceSerial() { ); if (!isEmulatorConfig) return 'INACTIVE CONFIG'; if (passedAdbSerial) return passedAdbSerial; - const connectedPhysicalDevices = getDeviceIds((deviceIdAndState) => { + const connectedPhysicalDevices = getDeviceIds(deviceIdAndState => { const [deviceId, state] = deviceIdAndState; if (deviceId.startsWith('emulator')) { return false; @@ -27,7 +27,7 @@ function resolveAttachedAndroidDeviceSerial() { console.warn( `Device "${deviceId}" has state "${state}", but state "device" is expected. This device will be ignored.`, ); - return false;; + return false; } }); if (connectedPhysicalDevices.length === 0) { @@ -96,7 +96,6 @@ function resolveAvdNameFromDeviceId(deviceId) { throw new Error(`Failed to get emulator name for id "${deviceId}"`); } - /** * @callback AdbDevicesFilterPredicate * @param {[string, string]} idAndState @@ -121,10 +120,10 @@ function getDeviceIds(filterPredicate = () => true) { throw new Error('The attached device list is empty'); } return adbDeviceLines - .map(line => /** @type {[string, string]} */(line.split('\t'))) + .map(line => /** @type {[string, string]} */ (line.split('\t'))) .filter(filterPredicate) - .map(deviceIdAndState => deviceIdAndState[0]) - } + .map(deviceIdAndState => deviceIdAndState[0]); +} module.exports = { detectAndroidEmulatorName, diff --git a/scripts/e2e/detox-utils.cjs b/scripts/e2e/detox-utils.cjs index 45d41c3277..65d0e1cd1f 100644 --- a/scripts/e2e/detox-utils.cjs +++ b/scripts/e2e/detox-utils.cjs @@ -4,7 +4,7 @@ const AndroidDeviceUtil = require('./android-devices'); const isRunningCI = process.env.CI != null; // Assumes that local development is done on arm64-v8a. -const apkBulidArchitecture = isRunningCI ? 'x86_64' : 'arm64-v8a'; +const apkBuildArchitecture = isRunningCI ? 'x86_64' : 'arm64-v8a'; // test-butler requires AOSP emulator image, which is not available to download for arm64-v8a in Android Studio SDK Manager, therefore // it is assumed here that arm64-v8a AOSP emulator is not available in local setup. const testButlerApkPath = isRunningCI @@ -76,13 +76,13 @@ function commonDetoxConfigFactory(applicationName) { 'android.debug': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', - build: `cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug -PreactNativeArchitectures=${apkBulidArchitecture} && cd ..`, + build: `cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug -PreactNativeArchitectures=${apkBuildArchitecture} && cd ..`, reversePorts: [8081], }, 'android.release': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', - build: `cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release -PreactNativeArchitectures=${apkBulidArchitecture} && cd ..`, + build: `cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release -PreactNativeArchitectures=${apkBuildArchitecture} && cd ..`, }, }, devices: { From 5615d4f71ab5208b00523aad09f063a9bdcb7dcd Mon Sep 17 00:00:00 2001 From: Krzysztof Wojnar Date: Tue, 25 Nov 2025 20:39:06 +0100 Subject: [PATCH 2/8] chore(e2e): e2e tests in FabricExamples adjustments --- .../e2e/component-objects/back-button.ts | 31 +++++++++++++++++++ FabricExample/e2e/e2e-utils.ts | 5 ++- FabricExample/e2e/issuesTests/Test2926.e2e.ts | 3 +- FabricExample/e2e/issuesTests/Test432.e2e.ts | 5 +-- FabricExample/e2e/issuesTests/Test528.e2e.ts | 4 ++- FabricExample/tsconfig.json | 5 ++- 6 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 FabricExample/e2e/component-objects/back-button.ts diff --git a/FabricExample/e2e/component-objects/back-button.ts b/FabricExample/e2e/component-objects/back-button.ts new file mode 100644 index 0000000000..f3c2946e84 --- /dev/null +++ b/FabricExample/e2e/component-objects/back-button.ts @@ -0,0 +1,31 @@ +import { device, element, by } from 'detox'; +import { getIOSVersion } from '../../../scripts/e2e/ios-devices.js'; +import semverSatisfies from 'semver/functions/satisfies'; +import semverCoerce from 'semver/functions/coerce'; + +const backButtonElement = element(by.id('BackButton')); + +export async function getBackButton() { + const platform = device.getPlatform(); + if (platform === 'ios') { + return getIOSBackButton(); + } else if (platform === 'android') { + return backButtonElement; + } else throw new Error(`Platform "${platform}" not supported`); +} +async function getIOSBackButton() { + const iosVersion = semverCoerce(getIOSVersion().replace('iOS', ''))!; + if (semverSatisfies(iosVersion, '>=26.0')) { + const elementsByAttributes = + (await backButtonElement.getAttributes()) as unknown as { + elements: { className: string }[]; + }; + const elements = elementsByAttributes.elements; + if (Array.isArray(elements)) { + return backButtonElement.atIndex( + elements.findIndex(elem => elem.className === '_UIButtonBarButton'), + ); + } + } + return backButtonElement; +} diff --git a/FabricExample/e2e/e2e-utils.ts b/FabricExample/e2e/e2e-utils.ts index 6e8cbf8fba..5fba9293b5 100644 --- a/FabricExample/e2e/e2e-utils.ts +++ b/FabricExample/e2e/e2e-utils.ts @@ -18,7 +18,10 @@ export async function selectTestScreen(screenName: string) { // More details: https://github.com/software-mansion/react-native-screens/pull/2919 await device.pressBack(); } else { - await element(by.type('UISearchBarTextField')).replaceText(screenName); + await waitFor(element(by.id('root-screen-tests-' + screenName))) + .toBeVisible() + .whileElement(by.id('root-screen-examples-scrollview')) + .scroll(600, 'down', Number.NaN, 0.85); } await expect(element(by.id(`root-screen-tests-${screenName}`))).toBeVisible(); diff --git a/FabricExample/e2e/issuesTests/Test2926.e2e.ts b/FabricExample/e2e/issuesTests/Test2926.e2e.ts index 9649497f05..cdc5e6af48 100644 --- a/FabricExample/e2e/issuesTests/Test2926.e2e.ts +++ b/FabricExample/e2e/issuesTests/Test2926.e2e.ts @@ -1,5 +1,6 @@ import { device, expect, element, by } from 'detox'; import { describeIfiOS, selectTestScreen } from '../e2e-utils'; +import { getBackButton } from '../component-objects/back-button'; // PR related to iOS search bar describeIfiOS('Test2926', () => { @@ -26,7 +27,7 @@ describeIfiOS('Test2926', () => { await element(by.type('UISearchBarTextField')).replaceText('Item 2'); await element(by.id('home-button-open-second')).tap(); - await element(by.id('BackButton')).tap(); + await (await getBackButton()).tap(); await expect(element(by.type('UISearchBarTextField'))).toBeVisible(); await expect(element(by.type('UISearchBarTextField'))).toHaveText('Item 2'); diff --git a/FabricExample/e2e/issuesTests/Test432.e2e.ts b/FabricExample/e2e/issuesTests/Test432.e2e.ts index ee083827e5..bd5261cf50 100644 --- a/FabricExample/e2e/issuesTests/Test432.e2e.ts +++ b/FabricExample/e2e/issuesTests/Test432.e2e.ts @@ -1,5 +1,6 @@ import { device, expect, element, by } from 'detox'; import { selectTestScreen } from '../e2e-utils'; +import { getBackButton } from '../component-objects/back-button'; describe('Test432', () => { beforeAll(async () => { @@ -26,7 +27,7 @@ describe('Test432', () => { await expect(element(by.id('details-headerRight-red'))).toBeVisible(100); if (device.getPlatform() === 'ios') { - await element(by.id('BackButton')).tap(); + await (await getBackButton()).tap(); } else { await device.pressBack(); } @@ -47,7 +48,7 @@ describe('Test432', () => { waitFor(element(by.id('info-headerRight-green-1'))).toBeVisible(100); if (device.getPlatform() === 'ios') { - await element(by.id('BackButton')).tap(); + await (await getBackButton()).tap(); } else { await device.pressBack(); } diff --git a/FabricExample/e2e/issuesTests/Test528.e2e.ts b/FabricExample/e2e/issuesTests/Test528.e2e.ts index 59030e10e3..533331b7e5 100644 --- a/FabricExample/e2e/issuesTests/Test528.e2e.ts +++ b/FabricExample/e2e/issuesTests/Test528.e2e.ts @@ -1,5 +1,6 @@ import { device, expect, element, by } from 'detox'; import { describeIfiOS, selectTestScreen } from '../e2e-utils'; +import { getBackButton } from '../component-objects/back-button'; // Detox currently supports orientation only on iOS describeIfiOS('Test528', () => { @@ -22,7 +23,8 @@ describeIfiOS('Test528', () => { it('headerRight button should be visible after coming back from horizontal screen', async () => { await element(by.text('Go to Screen 2')).tap(); await device.setOrientation('landscape'); - await element(by.id('BackButton')).tap(); + + await (await getBackButton()).tap(); await expect(element(by.text('Custom Button'))).toBeVisible(100); await device.setOrientation('portrait'); await expect(element(by.text('Custom Button'))).toBeVisible(100); diff --git a/FabricExample/tsconfig.json b/FabricExample/tsconfig.json index 3c43903cfd..8ff28ba3a4 100644 --- a/FabricExample/tsconfig.json +++ b/FabricExample/tsconfig.json @@ -1,3 +1,6 @@ { - "extends": "../tsconfig.json" + "extends": "../tsconfig.json", + "compilerOptions": { + "allowJs": true, + }, } From f67221964be067df19f3471403b0fd7042bb2eec Mon Sep 17 00:00:00 2001 From: Krzysztof Wojnar Date: Tue, 25 Nov 2025 20:42:03 +0100 Subject: [PATCH 3/8] chore(e2e): paper tests adjustments --- Example/e2e/examplesTests/bottomTabs.e2e.ts | 1 + Example/e2e/examplesTests/events.e2e.ts | 52 +++++++++------------ src/types.tsx | 2 +- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/Example/e2e/examplesTests/bottomTabs.e2e.ts b/Example/e2e/examplesTests/bottomTabs.e2e.ts index 4a0585867d..aafce23c51 100644 --- a/Example/e2e/examplesTests/bottomTabs.e2e.ts +++ b/Example/e2e/examplesTests/bottomTabs.e2e.ts @@ -3,6 +3,7 @@ import { device, expect, element, by } from 'detox'; describe('Bottom tabs and native stack', () => { beforeEach(async () => { await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); }); it('should go to main screen and back', async () => { diff --git a/Example/e2e/examplesTests/events.e2e.ts b/Example/e2e/examplesTests/events.e2e.ts index 8fb755d192..9dea07b120 100644 --- a/Example/e2e/examplesTests/events.e2e.ts +++ b/Example/e2e/examplesTests/events.e2e.ts @@ -11,43 +11,33 @@ const pressBack = async () => { }; const awaitClassicalEventBehavior = async () => { - if (device.getPlatform() === 'ios') { - await expect( - element(by.text('9. Chats | transitionStart | closing')), - ).toExist(); - await expect( - element(by.text('10. Privacy | transitionStart | closing')), - ).toExist(); - await expect( - element(by.text('11. Main | transitionStart | opening')), - ).toExist(); - await expect( - element(by.text('12. Chats | transitionEnd | closing')), - ).toExist(); - await expect( - element(by.text('13. Privacy | transitionEnd | closing')), - ).toExist(); - await expect(element(by.text('14. Privacy | beforeRemove'))).toExist(); - await expect(element(by.text('15. Chats | beforeRemove'))).toExist(); - await expect( - element(by.text('16. Main | transitionEnd | opening')), - ).toExist(); - } else { - await expect(element(by.text('9. Privacy | beforeRemove'))).toExist(); - await expect(element(by.text('10. Chats | beforeRemove'))).toExist(); - await expect( - element(by.text('11. Main | transitionStart | opening')), - ).toExist(); - await expect( - element(by.text('12. Main | transitionEnd | opening')), - ).toExist(); + const expectedEvents = + device.getPlatform() === 'ios' + ? [ + '9. Chats | transitionStart | closing', + '10. Privacy | transitionStart | closing', + '11. Main | transitionStart | opening', + '12. Chats | transitionEnd | closing', + '13. Privacy | transitionEnd | closing', + '14. Privacy | beforeRemove', + '15. Chats | beforeRemove', + '16. Main | transitionEnd | opening', + ] + : [ + '9. Privacy | beforeRemove', + '10. Chats | beforeRemove', + '11. Main | transitionStart | opening', + '12. Main | transitionEnd | opening', + ]; + for (const expectedEventNotiication of expectedEvents) { + await expect(element(by.text(expectedEventNotiication))).toExist(); } }; describe('Events', () => { beforeEach(async () => { await device.reloadReactNative(); - // await device.launchApp({ newInstance: true }); + await device.launchApp({ newInstance: true }); await waitFor(element(by.id('root-screen-playground-Events'))) .toBeVisible() diff --git a/src/types.tsx b/src/types.tsx index 1fb9af5145..df0e29d94d 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -394,7 +394,7 @@ export interface ScreenProps extends ViewProps { * There is also possibility to specify `fitToContents` literal, which intents to set the sheet height * to the height of its contents. On iOS `fitToContents` currently also includes small padding accounting for bottom inset. * - * Please note that the array **must** be sorted in ascending order. This invariant is verified only in developement mode, + * Please note that the array **must** be sorted in ascending order. This invariant is verified only in development mode, * where violation results in error. * * **Android is limited to up 3 values in the array** -- any surplus values, beside first three are ignored. From c73b2771a93a906e516c659917734ca7a346015a Mon Sep 17 00:00:00 2001 From: Krzysztof Wojnar Date: Thu, 27 Nov 2025 19:06:01 +0100 Subject: [PATCH 4/8] chore(e2e): widow spaces in e2e code --- .github/workflows/npm-screens-publish-nightly.yml | 2 +- Example/.gitignore | 2 +- Example/android/app/build.gradle | 2 +- apps/src/tests/Test3265.tsx | 2 +- apps/src/tests/Test556.tsx | 6 +++--- apps/src/tests/TestBottomTabsOrientation.tsx | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/npm-screens-publish-nightly.yml b/.github/workflows/npm-screens-publish-nightly.yml index a62be55a95..de5432b0dd 100644 --- a/.github/workflows/npm-screens-publish-nightly.yml +++ b/.github/workflows/npm-screens-publish-nightly.yml @@ -24,7 +24,7 @@ jobs: node-version: 22 cache: 'yarn' registry-url: https://registry.npmjs.org/ - + # Ensure npm 11.5.1 or later is installed for OIDC - name: Update npm run: npm install -g npm@latest diff --git a/Example/.gitignore b/Example/.gitignore index ec11000081..4b8d0da1f8 100644 --- a/Example/.gitignore +++ b/Example/.gitignore @@ -65,7 +65,7 @@ yarn-error.log # testing /coverage - + # Yarn .yarn/* !.yarn/patches diff --git a/Example/android/app/build.gradle b/Example/android/app/build.gradle index 654bea0a75..5b8c959a3e 100644 --- a/Example/android/app/build.gradle +++ b/Example/android/app/build.gradle @@ -49,7 +49,7 @@ react { // // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" // hermesFlags = ["-O", "-output-source-map"] - + /* Autolinking */ autolinkLibrariesWithApp() } diff --git a/apps/src/tests/Test3265.tsx b/apps/src/tests/Test3265.tsx index 1e831bb714..2f23f5f342 100644 --- a/apps/src/tests/Test3265.tsx +++ b/apps/src/tests/Test3265.tsx @@ -18,7 +18,7 @@ export default function TestScrollViewHorizontal() {