Skip to content

Commit 150121e

Browse files
LKuchnokligarski
andauthored
chore(test): e2e and scenario for tabs specialEffects scrollToTop (#3953)
## Description This PR adds a new Detox e2e test suite that validates the `specialEffects.repeatedTabSelection.scrollToTop` tab property and update the scenario _e2e test_ section. The Detox e2e tests covers all manual steps. New functions were added to e2e-utils.ts file to enable selecting already open tab on iOS26.2 and another one to allow run test just for android. Closes: software-mansion/react-native-screens-labs#1190 ## Changes - **FabricExample/e2e**: Added Detox e2e test suite `test-tabs-special-effects.e2e.ts` covering `scrollToTop: true`, `scrollToTop: false`, and default (no `specialEffects`) behavior; Added new functions into e2e-utils: `describeIfAndroid` and `forceTapByLabeliOS ` - **apps/tests/tabs**: Update`test-tabs-special-effects` scenario screen with e2e covarage information and index with testID values Update index with testIDs needed in e2e test --------- Co-authored-by: Krzysztof Ligarski <[email protected]>
1 parent 0683933 commit 150121e

4 files changed

Lines changed: 143 additions & 10 deletions

File tree

FabricExample/e2e/e2e-utils.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { device, expect, element, by } from 'detox';
2+
import { AndroidElementAttributes, IosElementAttributes } from 'detox/detox';
23

34
export const describeIfiOS =
45
device.getPlatform() === 'ios' ? describe : describe.skip;
56

7+
export const describeIfAndroid =
8+
device.getPlatform() === 'android' ? describe : describe.skip;
9+
610
async function scrollUntilVisible(id: string, scrollViewId: string) {
711
await waitFor(element(by.id(id)))
812
.toBeVisible()
@@ -62,3 +66,31 @@ export async function selectSingleFeatureTestsScreen(
6266
);
6367
await element(by.id(`${screenKey}`)).tap();
6468
}
69+
type ElementAttributes = IosElementAttributes | AndroidElementAttributes;
70+
71+
export async function getElementAttributes(
72+
testLabel: string,
73+
): Promise<ElementAttributes> {
74+
const attrs = await element(by.label(testLabel)).getAttributes();
75+
76+
if ('elements' in attrs) {
77+
throw new Error(
78+
`Multiple elements (${attrs.elements.length}) found for label: "${testLabel}". `
79+
);
80+
}
81+
82+
return attrs as ElementAttributes;
83+
}
84+
85+
/**
86+
* Performs a coordinate-based tap on iOS to interact with an element that may be
87+
* obstructed by other UI layers, bypassing Detox's default visibility checks.
88+
*/
89+
export async function forceTapByLabeliOS(testLabel: string) {
90+
const elementAttributes = await getElementAttributes(testLabel);
91+
const { x, y, width, height } = elementAttributes.frame;
92+
await device.tap({
93+
x: x + width / 2,
94+
y: y + height / 2,
95+
});
96+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { device, expect, element, by } from 'detox';
2+
import {
3+
selectSingleFeatureTestsScreen,
4+
forceTapByLabeliOS,
5+
} from '../../e2e-utils';
6+
7+
/**
8+
* Selects a tab bar item. On iOS, this uses a forced coordinate tap to
9+
* ensure the tab is selected even if it is obstructed by the iOS 26 Liquid Glass lens.
10+
*/
11+
async function forceSelectTabByLabel(label: string) {
12+
if (device.getPlatform() === 'ios') {
13+
await forceTapByLabeliOS(label);
14+
} else {
15+
await element(by.label(label)).tap();
16+
}
17+
}
18+
19+
describe('Tabs specialEffects — scrollToTop', () => {
20+
beforeAll(async () => {
21+
await device.reloadReactNative();
22+
await selectSingleFeatureTestsScreen(
23+
'Tabs',
24+
'test-tabs-special-effects-scroll-to-top',
25+
);
26+
});
27+
28+
it('should display tab bar and Tab1 scrollable list on load', async () => {
29+
await expect(element(by.id('tab1-scrollview'))).toBeVisible();
30+
await expect(element(by.id('tab1-item-1'))).toBeVisible();
31+
if (device.getPlatform() === 'ios') {
32+
await expect(element(by.type('UITabBar'))).toBeVisible();
33+
} else {
34+
await expect(element(by.id('tab1-tab-item'))).toBeVisible();
35+
await expect(element(by.id('tab2-tab-item'))).toBeVisible();
36+
await expect(element(by.id('tab3-tab-item'))).toBeVisible();
37+
}
38+
});
39+
40+
it('Tab1 (scrollToTop: true) — re-tapping active tab scrolls list back to top', async () => {
41+
await element(by.id('tab1-scrollview')).scroll(300, 'down', NaN, 0.85);
42+
await expect(element(by.id('tab1-item-1'))).not.toBeVisible();
43+
44+
await forceSelectTabByLabel('tab1-tab-item-label');
45+
46+
await waitFor(element(by.id('tab1-item-1')))
47+
.toBeVisible()
48+
.withTimeout(3000);
49+
});
50+
51+
it('Tab2 (scrollToTop: false) — re-tapping active tab preserves scroll position', async () => {
52+
await element(by.id('tab2-tab-item')).tap();
53+
await expect(element(by.id('tab2-item-1'))).toBeVisible();
54+
55+
await element(by.id('tab2-scrollview')).scroll(300, 'down', NaN, 0.85);
56+
await expect(element(by.id('tab2-item-1'))).not.toBeVisible();
57+
58+
await forceSelectTabByLabel('tab2-tab-item-label');
59+
60+
await expect(element(by.id('tab2-item-1'))).not.toBeVisible();
61+
});
62+
63+
it('Tab3 (no specialEffects) — re-tapping active tab scrolls list back to top', async () => {
64+
await element(by.id('tab3-tab-item')).tap();
65+
await expect(element(by.id('tab3-item-1'))).toBeVisible();
66+
67+
await element(by.id('tab3-scrollview')).scroll(300, 'down', NaN, 0.85);
68+
await expect(element(by.id('tab3-item-1'))).not.toBeVisible();
69+
70+
await forceSelectTabByLabel('tab3-tab-item-label');
71+
72+
await waitFor(element(by.id('tab3-item-1')))
73+
.toBeVisible()
74+
.withTimeout(3000);
75+
});
76+
77+
it('Tab1 (scrollToTop: true) — switching away and back preserves scroll position', async () => {
78+
await forceSelectTabByLabel('tab1-tab-item-label');
79+
await expect(element(by.id('tab1-item-1'))).toBeVisible();
80+
81+
await element(by.id('tab1-scrollview')).scroll(300, 'down', NaN, 0.85);
82+
await expect(element(by.id('tab1-item-1'))).not.toBeVisible();
83+
84+
await forceSelectTabByLabel('tab3-tab-item-label');
85+
await expect(element(by.id('tab3-item-1'))).toBeVisible();
86+
87+
await forceSelectTabByLabel('tab1-tab-item-label');
88+
89+
await expect(element(by.id('tab1-item-1'))).not.toBeVisible();
90+
});
91+
});

apps/src/tests/single-feature-tests/tabs/test-tabs-special-effects-scroll-to-top/index.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,23 @@ import {
99
} from '@apps/shared/gamma/containers/tabs';
1010

1111
const scenarioDescription: ScenarioDescription = {
12-
name: 'Tabs scrollToTop special effect',
12+
name: 'Tabs special effect scroll to top',
1313
key: 'test-tabs-special-effects-scroll-to-top',
1414
details:
15-
'Test settings of scrollToTop specialEffect.',
15+
'Test settings of specialEffect scrollToTop.',
1616
platforms: ['ios', 'android'],
1717
};
1818

19-
export function ScrollScreen() {
19+
interface ScrollScreenProps {
20+
tabName: string;
21+
}
22+
23+
export function ScrollScreen({ tabName }: ScrollScreenProps) {
2024
return (
21-
<ScrollView>
25+
<ScrollView testID={`${tabName}-scrollview`}>
2226
<Text style={styles.hint}>Scroll Screen — scroll down or re-tap the tab.</Text>
2327
{Array.from({ length: 50 }, (_, i) => (
24-
<Text key={i} style={styles.item}>
28+
<Text key={i} testID={`${tabName}-item-${i + 1}`} style={styles.item}>
2529
Item {i + 1}
2630
</Text>
2731
))}
@@ -32,10 +36,12 @@ export function ScrollScreen() {
3236
const TAB_CONFIGS: TabRouteConfig[] = [
3337
{
3438
name: 'Tab1',
35-
Component: ScrollScreen,
39+
Component: () => <ScrollScreen tabName="tab1" />,
3640
options: {
3741
...DEFAULT_TAB_ROUTE_OPTIONS,
3842
title: 'Tab1',
43+
tabBarItemTestID: 'tab1-tab-item',
44+
tabBarItemAccessibilityLabel: 'tab1-tab-item-label',
3945
specialEffects: {
4046
repeatedTabSelection: {
4147
scrollToTop: true,
@@ -45,10 +51,12 @@ const TAB_CONFIGS: TabRouteConfig[] = [
4551
},
4652
{
4753
name: 'Tab2',
48-
Component: ScrollScreen,
54+
Component: () => <ScrollScreen tabName="tab2" />,
4955
options: {
5056
...DEFAULT_TAB_ROUTE_OPTIONS,
5157
title: 'Tab2',
58+
tabBarItemTestID: 'tab2-tab-item',
59+
tabBarItemAccessibilityLabel: 'tab2-tab-item-label',
5260
specialEffects: {
5361
repeatedTabSelection: {
5462
scrollToTop: false
@@ -58,10 +66,12 @@ const TAB_CONFIGS: TabRouteConfig[] = [
5866
},
5967
{
6068
name: 'Tab3',
61-
Component: ScrollScreen,
69+
Component: () => <ScrollScreen tabName="tab3" />,
6270
options: {
6371
...DEFAULT_TAB_ROUTE_OPTIONS,
6472
title: 'Tab3',
73+
tabBarItemTestID: 'tab3-tab-item',
74+
tabBarItemAccessibilityLabel: 'tab3-tab-item-label',
6575
},
6676
},
6777
];

apps/src/tests/single-feature-tests/tabs/test-tabs-special-effects-scroll-to-top/scenario.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ that there is no scroll-to-top behavior when it is disabled or absent.
1111

1212
## E2E test
1313

14-
Other: ongoing research.
14+
Yes: Covers all manual scenario steps.
1515

1616
## Prerequisites
1717

@@ -28,7 +28,7 @@ list of 50 items.
2828

2929
### scrollToTop: true
3030

31-
1. Launch the app and navigate to the screen **Tabs specialEffects**.
31+
1. Launch the app and navigate to the screen **Tabs special effect scroll to top**.
3232

3333
- [ ] Expected: Three tabs (Tab1, Tab2, Tab3) are visible in the tab bar.
3434
Tab1 is active and displays a scrollable list of items.

0 commit comments

Comments
 (0)