Skip to content

Commit 34591a8

Browse files
authored
chore(iOS, Fabric): add tests for header interactions (#2848)
## Description This PR adds tests for #2809, #1589 and #2800. ## Changes The main assumption is to test all permutation of headerBackButtonMenuEnabled, headerBackButtonDisplayMode and content of button (default - taken from previous screen headerTitle, custom from headerBackTitle and styled when there are styles applied). I hope this will help us tracking regression as this part of the code is a bit confusing and caused us problem in the past. Additionally there are added "custom" scenarios that test default default behavior with default text and custom text (if the back button changes it's appearance depending on available space and [headerBackButtonDisplayMode](https://developer.apple.com/documentation/uikit/uinavigationitem/backbuttondisplaymode-swift.property) specification). The some of the last cases are set to failing, and this is known disparity reported in #2809, we plan to fix it soon after, but wonted to make sure we don't cause a regression. **Remark:** I did it in a way that after each test we're returning to the screen with all test cases for 2809, this is due to the fact that scrolling through list of all Test* takes a lot and reloading RN after each test would cause huge overhead. The downside of it is that if any of the test fails if will cascade fail all other. ## Test code and steps to reproduce You can run test with: ``` detox build --configuration ios.sim.debug detox test --configuration ios.sim.debug e2e/issuesTests/Test2809.e2e.ts -l debug ```` ## Checklist - [ ] Ensured that CI passes
1 parent ef7b1d3 commit 34591a8

File tree

4 files changed

+466
-0
lines changed

4 files changed

+466
-0
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { device, expect, element, by } from 'detox';
2+
import { describeIfiOS } from '../e2e-utils';
3+
4+
const expectBackButtonMenuWithTheSameLabel = async (text: string) => {
5+
await element(by.text(text)).longPressAndDrag(700, NaN, NaN, element(by.text('VOID')), NaN, NaN, "fast", 0); // open
6+
await waitFor(element(by.type('_UIContextMenuView'))).toBeVisible();
7+
await expect(element(by.text(text).withAncestor(by.type('_UIContextMenuView')))).toBeVisible();
8+
await element(by.text('VOID')).tap(); // close
9+
await waitFor(element(by.type('_UIContextMenuView'))).not.toBeVisible();
10+
await waitFor(element(by.text(text)).atIndex(1)).not.toExist();
11+
await expect(element(by.text(text)).atIndex(0)).toBeVisible();
12+
};
13+
14+
const expectBackButtonMenuWithDifferentLabels = async (buttonTitle: string, backButtonMenuLabel: string) => {
15+
await element(by.text(buttonTitle)).longPressAndDrag(700, NaN, NaN, element(by.text('VOID')), NaN, NaN, "fast", 0); // open
16+
await waitFor(element(by.type('_UIContextMenuView'))).toBeVisible();
17+
await expect(element(by.text(backButtonMenuLabel).withAncestor(by.type('_UIContextMenuView')))).toBeVisible();
18+
await element(by.text('VOID')).tap(); // close
19+
await waitFor(element(by.type('_UIContextMenuView'))).not.toBeVisible();
20+
await waitFor(element(by.text(backButtonMenuLabel))).not.toBeVisible();
21+
await expect(element(by.text(buttonTitle))).toBeVisible();
22+
};
23+
24+
const expectBackButtonMenuIconAndLabel = async (buttonId: string, backButtonMenuLabel: string) => {
25+
await element(by.id(buttonId)).longPressAndDrag(700, NaN, NaN, element(by.text('VOID')), NaN, NaN, "fast", 0); // open
26+
await expect(element(by.text(backButtonMenuLabel).withAncestor(by.type('_UIContextMenuView')))).toBeVisible();
27+
await waitFor(element(by.type('_UIContextMenuView'))).toBeVisible();
28+
await element(by.text('VOID')).tap(); // close
29+
await waitFor(element(by.type('_UIContextMenuView'))).not.toBeVisible();
30+
};
31+
32+
const expectBackButtonMenuToNotExistOnLabel = async (text: string) => {
33+
await element(by.text(text)).longPressAndDrag(700, NaN, NaN, element(by.text('VOID')), NaN, NaN, "fast", 0); // open
34+
await expect(element(by.type('_UIContextMenuView'))).not.toExist();
35+
};
36+
37+
const expectInitialPageToExist = async (testName: string, expectToExist: Detox.NativeMatcher, expectToNotExist?: Detox.NativeMatcher) => {
38+
await element(by.text(testName)).tap();
39+
await element(by.text('Open screen')).tap();
40+
await expect(element(expectToExist)).toBeVisible();
41+
if (expectToNotExist) {
42+
await expect(element(expectToNotExist)).not.toBeVisible();
43+
}
44+
};
45+
46+
describeIfiOS('Test2809', () => {
47+
beforeAll(async () => {
48+
await device.reloadReactNative();
49+
});
50+
51+
it('Test2809 should exist', async () => {
52+
await waitFor(element(by.id('root-screen-tests-Test2809')))
53+
.toBeVisible()
54+
.whileElement(by.id('root-screen-examples-scrollview'))
55+
.scroll(600, 'down', NaN, 0.85);
56+
57+
await expect(element(by.id('root-screen-tests-Test2809'))).toBeVisible();
58+
await element(by.id('root-screen-tests-Test2809')).tap();
59+
});
60+
61+
describe('backButtonMenuEnabled: true', () => {
62+
afterEach(async () => {
63+
await element(by.text('Pop to top')).tap(); // Go back
64+
});
65+
66+
describe('backButtonDisplayMode: default', () => {
67+
it('default text stays visible and matches label in back button menu', async () => {
68+
await expectInitialPageToExist('EnabledDefaultDefaultText', by.text('First'));
69+
await expectBackButtonMenuWithTheSameLabel('First'); // Check if backButtonMenu works
70+
});
71+
72+
it('custom text stays visible and matches label in back button menu', async () => {
73+
await expectInitialPageToExist('EnabledDefaultCustomText', by.text('Custom'));
74+
await expectBackButtonMenuWithTheSameLabel('Custom'); // Check if backButtonMenu works
75+
});
76+
77+
// We don't check if the styles are applied, I think that it could be flaky, but it can be done
78+
// using element(by.type('UIButtonLabel')).getAttributes() and looking at bounds. The values there
79+
// doesn't match the ones in the code exactly (I think some padding is added), so I don't think we should do that.
80+
it('styled text stays visible and matches label in back button menu', async () => {
81+
await expectInitialPageToExist('EnabledDefaultStyledText', by.text('First'));
82+
await expectBackButtonMenuWithTheSameLabel('First'); // Check if backButtonMenu works
83+
});
84+
});
85+
86+
describe('backButtonDisplayMode: generic', () => {
87+
it('default text is truncated by backButtonDisplayMode and is used in back button menu', async () => {
88+
await expectInitialPageToExist('EnabledGenericDefaultText', by.text('Back'));
89+
await expectBackButtonMenuWithDifferentLabels('Back', 'First'); // Check if backButtonMenu works
90+
});
91+
92+
// TODO: We should be able to fix that
93+
// Custom text overrides backButtonDisplayMode
94+
it('custom text is NOT truncated by backButtonDisplayMode and is used in back button menu', async () => {
95+
await expectInitialPageToExist('EnabledGenericCustomText', by.text('Custom'));
96+
await expectBackButtonMenuWithTheSameLabel('Custom'); // Check if backButtonMenu works
97+
});
98+
99+
// Custom styles override backButtonDisplayMode
100+
it('styled text is NOT truncated by backButtonDisplayMode and is used in back button menu', async () => {
101+
await expectInitialPageToExist('EnabledGenericStyledText', by.text('First'));
102+
await expectBackButtonMenuWithTheSameLabel('First'); // Check if backButtonMenu works
103+
});
104+
});
105+
106+
describe('backButtonDisplayMode: minimal', () => {
107+
it('chevron is used as back button and default text is used in back button menu', async () => {
108+
await expectInitialPageToExist('EnabledMinimalDefaultText', by.id('chevron.backward'), by.text('First'));
109+
await expectBackButtonMenuIconAndLabel('chevron.backward', 'First'); // Check if backButtonMenu works
110+
});
111+
112+
it('chevron is used as back button and custom text is used in back button menu', async () => {
113+
await expectInitialPageToExist('EnabledMinimalCustomText', by.id('chevron.backward'), by.text('Custom'));
114+
await expectBackButtonMenuIconAndLabel('chevron.backward', 'Custom'); // Check if backButtonMenu works
115+
});
116+
117+
it('styles are omitted, chevron is used as back button and default text is used in back button menu', async () => {
118+
await expectInitialPageToExist('EnabledMinimalStyledText', by.id('chevron.backward'), by.text('First'));
119+
await expectBackButtonMenuIconAndLabel('chevron.backward', 'First'); // Check if backButtonMenu works
120+
});
121+
});
122+
});
123+
124+
describe('backButtonMenuEnabled: false', () => {
125+
afterEach(async () => {
126+
await element(by.text('Pop to top')).tap(); // Go back
127+
});
128+
129+
describe('backButtonDisplayMode: default', () => {
130+
it('default text stays visible and back button menu is disabled', async () => {
131+
await expectInitialPageToExist('DisabledDefaultDefaultText', by.text('First'));
132+
await expectBackButtonMenuToNotExistOnLabel('First'); // Check if backButtonMenu is disabled
133+
});
134+
135+
it('custom text stays visible and back button menu is disabled', async () => {
136+
await expectInitialPageToExist('DisabledDefaultCustomText', by.text('Custom'));
137+
await expectBackButtonMenuToNotExistOnLabel('Custom'); // Check if backButtonMenu is disabled
138+
});
139+
140+
it('styled text stays visible and back button menu is disabled', async () => {
141+
await expectInitialPageToExist('DisabledDefaultStyledText', by.text('First'));
142+
await expectBackButtonMenuToNotExistOnLabel('First'); // Check if backButtonMenu is disabled
143+
});
144+
});
145+
146+
// [backButtonMenuEnabled: false and backButtonDisplayMode: generic]
147+
// generic is not working as currently backButtonDisplayMode is causing backButtonItem to be set
148+
describe('backButtonDisplayMode: generic', () => {
149+
it('default text is visible, because backButtonDisplayMode is overwritten by backButtonDisplayMode', async () => {
150+
await expectInitialPageToExist('DisabledGenericDefaultText', by.text('First'));
151+
await expectBackButtonMenuToNotExistOnLabel('First'); // Check if backButtonMenu is disabled
152+
});
153+
154+
it('custom text is visible, because backButtonDisplayMode is overwritten by backButtonDisplayMode', async () => {
155+
await expectInitialPageToExist('DisabledGenericCustomText', by.text('Custom'));
156+
await expectBackButtonMenuToNotExistOnLabel('Custom'); // Check if backButtonMenu is disabled
157+
});
158+
159+
it('styled text is visible, because backButtonDisplayMode is overwritten by backButtonDisplayMode', async () => {
160+
await expectInitialPageToExist('DisabledGenericStyledText', by.text('First'));
161+
await expectBackButtonMenuToNotExistOnLabel('First'); // Check if backButtonMenu is disabled
162+
});
163+
});
164+
165+
// [backButtonMenuEnabled: false and backButtonDisplayMode: minimal]
166+
// backButtonDisplayMode: minimal works as a kill switch so backButtonMenu value is omitted
167+
describe('backButtonDisplayMode: minimal', () => {
168+
it('chevron is used as back button and default text is used in back button menu, backButtonMenuEnabled is omitted', async () => {
169+
await expectInitialPageToExist('DisabledMinimalDefaultText', by.id('chevron.backward'), by.text('First'));
170+
await expectBackButtonMenuIconAndLabel('chevron.backward', 'First'); // Check if backButtonMenu works
171+
});
172+
173+
it('chevron is used as back button and custom text is used in back button menu, backButtonMenuEnabled is omitted', async () => {
174+
await expectInitialPageToExist('DisabledMinimalCustomText', by.id('chevron.backward'), by.text('Custom'));
175+
await expectBackButtonMenuIconAndLabel('chevron.backward', 'Custom'); // Check if backButtonMenu works
176+
});
177+
178+
it('backButtonMenuEnabled is omitted, chevron is used as back button and default text is used in back button menu', async () => {
179+
await expectInitialPageToExist('DisabledMinimalStyledText', by.id('chevron.backward'), by.text('First'));
180+
await expectBackButtonMenuIconAndLabel('chevron.backward', 'First'); // Check if backButtonMenu works
181+
});
182+
});
183+
});
184+
185+
// Custom
186+
it('Default long back label should be truncated to generic by backButtonDisplayMode', async () => {
187+
await expectInitialPageToExist('CustomLongDefaultText', by.text('Back'));
188+
await expectBackButtonMenuWithDifferentLabels('Back', 'LongLongLongLongLong'); // Check if backButtonMenu works
189+
await element(by.text('Pop to top')).tap(); // Go back
190+
});
191+
192+
it('Default label should be truncated to minimal by backButtonDisplayMode when title is long', async () => {
193+
await expectInitialPageToExist('CustomDefaultTextWithLongTitle', by.id('chevron.backward'), by.text('First'));
194+
await expectBackButtonMenuIconAndLabel('chevron.backward', 'First'); // Check if backButtonMenu works
195+
await element(by.text('Pop to top')).tap(); // Go back
196+
});
197+
198+
// TODO: We should be able to fix that
199+
// Custom text overrides backButtonDisplayMode because of using backButtonItem
200+
it.failing('Custom long back label should be truncated to generic by backButtonDisplayMode', async () => {
201+
await expectInitialPageToExist('CustomLongCustomText', by.text('Back'));
202+
await expectBackButtonMenuWithDifferentLabels('Back', 'LongLongLongLongLong'); // Check if backButtonMenu works
203+
await element(by.text('Pop to top')).tap(); // Go back
204+
});
205+
206+
// TODO: We should be able to fix that
207+
// Custom text overrides backButtonDisplayMode because of using backButtonItem
208+
it.failing('Custom back label should be truncated to minimal by backButtonDisplayMode when title is long', async () => {
209+
await expectInitialPageToExist('CustomCustomTextWithLongTitle', by.id('chevron.backward'), by.text('CustomBack'));
210+
await expectBackButtonMenuIconAndLabel('chevron.backward', 'CustomBack'); // Check if backButtonMenu works
211+
await element(by.text('Pop to top')).tap(); // Go back
212+
});
213+
});

apps/src/tests/Test2809/Shared.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as React from 'react';
2+
import { Button, Text, View } from 'react-native';
3+
import { ParamListBase } from '@react-navigation/native';
4+
import {
5+
createNativeStackNavigator,
6+
NativeStackNavigationOptions,
7+
NativeStackNavigationProp,
8+
} from '@react-navigation/native-stack';
9+
10+
11+
export function FinalScreen({
12+
navigation,
13+
}: {
14+
navigation: NativeStackNavigationProp<ParamListBase>;
15+
}) {
16+
return (
17+
<View style={{ flex: 1, backgroundColor: 'yellow', justifyContent: 'center', alignItems: 'center' }}>
18+
<Text>VOID</Text>
19+
<Button title="Pop to top" onPress={() => navigation.popTo('Home')} />
20+
</View>
21+
);
22+
}
23+
24+
export function Home({
25+
navigation,
26+
}: {
27+
navigation: NativeStackNavigationProp<ParamListBase>;
28+
}) {
29+
return (
30+
<View style={{ flex: 1, backgroundColor: 'yellow' }}>
31+
<Button
32+
title="Open screen"
33+
onPress={() => navigation.navigate('Second')}
34+
/>
35+
</View>
36+
);
37+
}
38+
39+
export const createStackWithOptions = (options1: NativeStackNavigationOptions, options2: NativeStackNavigationOptions) => () => {
40+
const Stack = createNativeStackNavigator();
41+
42+
return (
43+
<Stack.Navigator>
44+
<Stack.Screen name="First" component={Home} options={options1} />
45+
<Stack.Screen name="Second" component={FinalScreen} options={options2} />
46+
</Stack.Navigator>
47+
);
48+
};

0 commit comments

Comments
 (0)