Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/npm-screens-publish-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ yarn-error.log

# testing
/coverage

# Yarn
.yarn/*
!.yarn/patches
Expand Down
2 changes: 1 addition & 1 deletion Example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
2 changes: 1 addition & 1 deletion Example/e2e/examplesTests/bottomTabs.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { device, expect, element, by } from 'detox';

describe('Bottom tabs and native stack', () => {
beforeEach(async () => {
await device.reloadReactNative();
await device.launchApp({ newInstance: true });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a question here. Why do we relaunch application instead of reloading RN? This was done mostly to save CI time IIRC, so it is a subject to be changed, but I need reasoning here.

});

it('should go to main screen and back', async () => {
Expand Down
53 changes: 21 additions & 32 deletions Example/e2e/examplesTests/events.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,32 @@ 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 expectedEventNotification of expectedEvents) {
await expect(element(by.text(expectedEventNotification))).toExist();
}
};

describe('Events', () => {
beforeEach(async () => {
await device.reloadReactNative();
// await device.launchApp({ newInstance: true });
await device.launchApp({ newInstance: true });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, and in every other place, where it has been done.


await waitFor(element(by.id('root-screen-playground-Events')))
.toBeVisible()
Expand Down
35 changes: 35 additions & 0 deletions FabricExample/e2e/component-objects/back-button.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder about naming here. What are component-objects? Shouldn't be this kind of util directory with locators.js / actions.js?

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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 IOS_BAR_BUTTON_TYPE = '_UIButtonBarButton';
const backButtonElement = element(by.id('BackButton'));

export async function tapBarBackButton() {
const platform = device.getPlatform();
if (platform === 'ios') {
return (await getIOSBackButton()).tap();
} else if (platform === 'android') {
return backButtonElement.tap();
} else throw new Error(`Platform "${platform}" not supported`);
}
async function getIOSBackButton() {
const iosVersion = semverCoerce(getIOSVersion().replace('iOS', ''))!;
if (semverSatisfies(iosVersion, '>=26.0')) {
Comment on lines +18 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we need semver for this? On the other hand, it's just the example app so I guess that it's okay.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to save some time and several lines of code by adding the dev dependency, but I agree - it is an overkill and we can parse MAJOR.MINOR (or just bare MAJOR) oursevles. I don't have a strong opinion about it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @kkafar

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we don't need external dependency for this. Simple helper function is enough for that. Given what is going on with NPM & supply chain attacks recently -> lets avoid any unnecessary dependencies.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this function work only on iOS 26+?

const elementsByAttributes =
(await backButtonElement.getAttributes()) as unknown as {
elements: { className: string }[];
};
Comment on lines +21 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await backButtonElement.getAttributes() returns Detox.IosElementAttributes | Detox.AndroidElementAttributes | { elements: Detox.IosElementAttributes[]; } | { elements: Detox.AndroidElementAttributes[]; }, maybe we can handle this type somehow instead of forcing a type.

Copy link
Contributor Author

@KrzysztofWojnar KrzysztofWojnar Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the tough one.
It is an iOS-version-specific, behavior-based implementation for a technical selector, BUT QUITE READABLE (comparing to my other options). The biggest problem with the 'className' field is that it is not documented (I'm using the Hyrum's law).
It may break in future versions, but I can't predict it and finding this header back button in another way (filtering by other properties) is equally uncertain (or may break even easier). I don't have more robust solutions, you can call it a skill issue.
This probably won't make you feel any better, but I don't like it either.

Copy link
Contributor Author

@KrzysztofWojnar KrzysztofWojnar Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I can use magic numbers alternatively (backButtonElement.atIndex(0)).
I bet most people would do this in this case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I see what you mean. I think the current version is all right then.

const elements = elementsByAttributes.elements;
if (Array.isArray(elements)) {
const uiBarButtonIndex = elements.findIndex(
elem => elem.className === IOS_BAR_BUTTON_TYPE,
);
if (uiBarButtonIndex !== -1) {
return backButtonElement.atIndex(uiBarButtonIndex);
}
}
}
return backButtonElement;
}
5 changes: 4 additions & 1 deletion FabricExample/e2e/e2e-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
11 changes: 11 additions & 0 deletions FabricExample/e2e/helpers/disableStylus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getCommandLineResponse } from "react-native-screens/scripts/e2e/command-line-helpers";
import { device } from 'detox';

export function disableStylusPopupOnAndroid() {
if (device.getPlatform() === 'ios') return;
try {
getCommandLineResponse(`adb -s ${device.id} shell settings put secure stylus_handwriting_enabled 0`);
} catch {
console.warn('Failed to disable stylus setting.');
}
}
3 changes: 2 additions & 1 deletion FabricExample/e2e/issuesTests/Test2926.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { device, expect, element, by } from 'detox';
import { describeIfiOS, selectTestScreen } from '../e2e-utils';
import { tapBarBackButton } from '../component-objects/back-button';

// PR related to iOS search bar
describeIfiOS('Test2926', () => {
Expand All @@ -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 tapBarBackButton();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an iOS only test. We have nice & clean way to detect a back button & tap it & we replace it with the complicated logic in component-objects/back-button.ts? Why do we do this? My guess would be that this version does not work, but I need confirmation & explanation.


await expect(element(by.type('UISearchBarTextField'))).toBeVisible();
await expect(element(by.type('UISearchBarTextField'))).toHaveText('Item 2');
Expand Down
5 changes: 3 additions & 2 deletions FabricExample/e2e/issuesTests/Test432.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { device, expect, element, by } from 'detox';
import { selectTestScreen } from '../e2e-utils';
import { tapBarBackButton } from '../component-objects/back-button';

describe('Test432', () => {
beforeAll(async () => {
Expand All @@ -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 tapBarBackButton();
} else {
await device.pressBack();
}
Expand All @@ -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 tapBarBackButton();
} else {
await device.pressBack();
}
Expand Down
4 changes: 3 additions & 1 deletion FabricExample/e2e/issuesTests/Test528.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { device, expect, element, by } from 'detox';
import { describeIfiOS, selectTestScreen } from '../e2e-utils';
import { tapBarBackButton } from '../component-objects/back-button';

// Detox currently supports orientation only on iOS
describeIfiOS('Test528', () => {
Expand All @@ -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 tapBarBackButton();
await expect(element(by.text('Custom Button'))).toBeVisible(100);
await device.setOrientation('portrait');
await expect(element(by.text('Custom Button'))).toBeVisible(100);
Expand Down
2 changes: 2 additions & 0 deletions FabricExample/e2e/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { device } from 'detox';
import { disableStylusPopupOnAndroid } from './helpers/disableStylus';

beforeAll(async () => {
await device.launchApp();
disableStylusPopupOnAndroid();
});

afterAll(async () => {
Expand Down
2 changes: 2 additions & 0 deletions FabricExample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@
"@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",
"jest": "^29.6.3",
"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"
},
Expand Down
5 changes: 4 additions & 1 deletion FabricExample/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"extends": "../tsconfig.json"
"extends": "../tsconfig.json",
"compilerOptions": {
"allowJs": true,
},
}
18 changes: 18 additions & 0 deletions FabricExample/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion apps/src/tests/Test3265.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function TestScrollViewHorizontal() {
<Stack.Screen name='Home' component={(props: { navigation: StackNavigationProps }) => <View>
<Button onPress={() => props.navigation.navigate('ScrollView')} title='Go to ScrollView' />
</View>} />
<Stack.Screen name="ScrollView" component={() =>
<Stack.Screen name="ScrollView" component={() =>
<ScrollView horizontal={ true } >
<View style={{ width: 300 }}>
<Text style={{ fontSize: 48 }}>{ Array.from({ length: 100 }).map( _ => ['🤖', '👨‍💻', '👾'][Math.floor(Math.random() * 3)]).join('')}</Text>
Expand Down
6 changes: 3 additions & 3 deletions apps/src/tests/Test556.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function Second({ navigation }: ScreenBaseProps) {
});
}}
/>
<Button
<Button
title="Use multiple right items"
onPress={() => {
navigation.setOptions({
Expand All @@ -115,7 +115,7 @@ function Second({ navigation }: ScreenBaseProps) {
<Text>Right-2</Text>
</View>
)
},
},
{
customView: () => (
<View
Expand All @@ -128,7 +128,7 @@ function Second({ navigation }: ScreenBaseProps) {
)
}
]
})
})
}} />

<Button
Expand Down
4 changes: 2 additions & 2 deletions apps/src/tests/TestBottomTabsOrientation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function makeTabConfigs(
ios: {
type: 'sfSymbol',
name: 'house.fill',
},
},
android: {
type: 'imageSource',
imageSource: require('../../assets/variableIcons/icon_fill.png'),
Expand All @@ -94,7 +94,7 @@ function makeTabConfigs(
ios: {
type: 'templateSource',
templateSource: require('../../assets/variableIcons/icon.png'),
},
},
android: {
type: 'drawableResource',
name: 'sym_call_missed',
Expand Down
11 changes: 5 additions & 6 deletions scripts/e2e/android-devices.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -96,7 +96,6 @@ function resolveAvdNameFromDeviceId(deviceId) {
throw new Error(`Failed to get emulator name for id "${deviceId}"`);
}


/**
* @callback AdbDevicesFilterPredicate
* @param {[string, string]} idAndState
Expand All @@ -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,
Expand Down
Loading
Loading