Skip to content

chore(iOS, Fabric): add tests for header interactions #2848

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 11, 2025
Merged
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
213 changes: 213 additions & 0 deletions FabricExample/e2e/issuesTests/Test2809.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { device, expect, element, by } from 'detox';
import { describeIfiOS } from '../e2e-utils';

const expectBackButtonMenuWithTheSameLabel = async (text: string) => {
await element(by.text(text)).longPressAndDrag(700, NaN, NaN, element(by.text('VOID')), NaN, NaN, "fast", 0); // open
await waitFor(element(by.type('_UIContextMenuView'))).toBeVisible();
await expect(element(by.text(text).withAncestor(by.type('_UIContextMenuView')))).toBeVisible();
await element(by.text('VOID')).tap(); // close
await waitFor(element(by.type('_UIContextMenuView'))).not.toBeVisible();
await waitFor(element(by.text(text)).atIndex(1)).not.toExist();
await expect(element(by.text(text)).atIndex(0)).toBeVisible();
};

const expectBackButtonMenuWithDifferentLabels = async (buttonTitle: string, backButtonMenuLabel: string) => {
await element(by.text(buttonTitle)).longPressAndDrag(700, NaN, NaN, element(by.text('VOID')), NaN, NaN, "fast", 0); // open
await waitFor(element(by.type('_UIContextMenuView'))).toBeVisible();
await expect(element(by.text(backButtonMenuLabel).withAncestor(by.type('_UIContextMenuView')))).toBeVisible();
await element(by.text('VOID')).tap(); // close
await waitFor(element(by.type('_UIContextMenuView'))).not.toBeVisible();
await waitFor(element(by.text(backButtonMenuLabel))).not.toBeVisible();
await expect(element(by.text(buttonTitle))).toBeVisible();
};

const expectBackButtonMenuIconAndLabel = async (buttonId: string, backButtonMenuLabel: string) => {
await element(by.id(buttonId)).longPressAndDrag(700, NaN, NaN, element(by.text('VOID')), NaN, NaN, "fast", 0); // open
await expect(element(by.text(backButtonMenuLabel).withAncestor(by.type('_UIContextMenuView')))).toBeVisible();
await waitFor(element(by.type('_UIContextMenuView'))).toBeVisible();
await element(by.text('VOID')).tap(); // close
await waitFor(element(by.type('_UIContextMenuView'))).not.toBeVisible();
};

const expectBackButtonMenuToNotExistOnLabel = async (text: string) => {
await element(by.text(text)).longPressAndDrag(700, NaN, NaN, element(by.text('VOID')), NaN, NaN, "fast", 0); // open
await expect(element(by.type('_UIContextMenuView'))).not.toExist();
};

const expectInitialPageToExist = async (testName: string, expectToExist: Detox.NativeMatcher, expectToNotExist?: Detox.NativeMatcher) => {
await element(by.text(testName)).tap();
await element(by.text('Open screen')).tap();
await expect(element(expectToExist)).toBeVisible();
if (expectToNotExist) {
await expect(element(expectToNotExist)).not.toBeVisible();
}
};

describeIfiOS('Test2809', () => {
beforeAll(async () => {
await device.reloadReactNative();
});

it('Test2809 should exist', async () => {
await waitFor(element(by.id('root-screen-tests-Test2809')))
.toBeVisible()
.whileElement(by.id('root-screen-examples-scrollview'))
.scroll(600, 'down', NaN, 0.85);

await expect(element(by.id('root-screen-tests-Test2809'))).toBeVisible();
await element(by.id('root-screen-tests-Test2809')).tap();
});

describe('backButtonMenuEnabled: true', () => {
afterEach(async () => {
await element(by.text('Pop to top')).tap(); // Go back
});

describe('backButtonDisplayMode: default', () => {
it('default text stays visible and matches label in back button menu', async () => {
await expectInitialPageToExist('EnabledDefaultDefaultText', by.text('First'));
await expectBackButtonMenuWithTheSameLabel('First'); // Check if backButtonMenu works
});

it('custom text stays visible and matches label in back button menu', async () => {
await expectInitialPageToExist('EnabledDefaultCustomText', by.text('Custom'));
await expectBackButtonMenuWithTheSameLabel('Custom'); // Check if backButtonMenu works
});

// We don't check if the styles are applied, I think that it could be flaky, but it can be done
// using element(by.type('UIButtonLabel')).getAttributes() and looking at bounds. The values there
// doesn't match the ones in the code exactly (I think some padding is added), so I don't think we should do that.
it('styled text stays visible and matches label in back button menu', async () => {
await expectInitialPageToExist('EnabledDefaultStyledText', by.text('First'));
await expectBackButtonMenuWithTheSameLabel('First'); // Check if backButtonMenu works
});
});

describe('backButtonDisplayMode: generic', () => {
it('default text is truncated by backButtonDisplayMode and is used in back button menu', async () => {
await expectInitialPageToExist('EnabledGenericDefaultText', by.text('Back'));
await expectBackButtonMenuWithDifferentLabels('Back', 'First'); // Check if backButtonMenu works
});

// TODO: We should be able to fix that
// Custom text overrides backButtonDisplayMode
it('custom text is NOT truncated by backButtonDisplayMode and is used in back button menu', async () => {
await expectInitialPageToExist('EnabledGenericCustomText', by.text('Custom'));
await expectBackButtonMenuWithTheSameLabel('Custom'); // Check if backButtonMenu works
});

// Custom styles override backButtonDisplayMode
it('styled text is NOT truncated by backButtonDisplayMode and is used in back button menu', async () => {
await expectInitialPageToExist('EnabledGenericStyledText', by.text('First'));
await expectBackButtonMenuWithTheSameLabel('First'); // Check if backButtonMenu works
});
});

describe('backButtonDisplayMode: minimal', () => {
it('chevron is used as back button and default text is used in back button menu', async () => {
await expectInitialPageToExist('EnabledMinimalDefaultText', by.id('chevron.backward'), by.text('First'));
await expectBackButtonMenuIconAndLabel('chevron.backward', 'First'); // Check if backButtonMenu works
});

it('chevron is used as back button and custom text is used in back button menu', async () => {
await expectInitialPageToExist('EnabledMinimalCustomText', by.id('chevron.backward'), by.text('Custom'));
await expectBackButtonMenuIconAndLabel('chevron.backward', 'Custom'); // Check if backButtonMenu works
});

it('styles are omitted, chevron is used as back button and default text is used in back button menu', async () => {
await expectInitialPageToExist('EnabledMinimalStyledText', by.id('chevron.backward'), by.text('First'));
await expectBackButtonMenuIconAndLabel('chevron.backward', 'First'); // Check if backButtonMenu works
});
});
});

describe('backButtonMenuEnabled: false', () => {
afterEach(async () => {
await element(by.text('Pop to top')).tap(); // Go back
});

describe('backButtonDisplayMode: default', () => {
it('default text stays visible and back button menu is disabled', async () => {
await expectInitialPageToExist('DisabledDefaultDefaultText', by.text('First'));
await expectBackButtonMenuToNotExistOnLabel('First'); // Check if backButtonMenu is disabled
});

it('custom text stays visible and back button menu is disabled', async () => {
await expectInitialPageToExist('DisabledDefaultCustomText', by.text('Custom'));
await expectBackButtonMenuToNotExistOnLabel('Custom'); // Check if backButtonMenu is disabled
});

it('styled text stays visible and back button menu is disabled', async () => {
await expectInitialPageToExist('DisabledDefaultStyledText', by.text('First'));
await expectBackButtonMenuToNotExistOnLabel('First'); // Check if backButtonMenu is disabled
});
});

// [backButtonMenuEnabled: false and backButtonDisplayMode: generic]
// generic is not working as currently backButtonDisplayMode is causing backButtonItem to be set
describe('backButtonDisplayMode: generic', () => {
it('default text is visible, because backButtonDisplayMode is overwritten by backButtonDisplayMode', async () => {
await expectInitialPageToExist('DisabledGenericDefaultText', by.text('First'));
await expectBackButtonMenuToNotExistOnLabel('First'); // Check if backButtonMenu is disabled
});

it('custom text is visible, because backButtonDisplayMode is overwritten by backButtonDisplayMode', async () => {
await expectInitialPageToExist('DisabledGenericCustomText', by.text('Custom'));
await expectBackButtonMenuToNotExistOnLabel('Custom'); // Check if backButtonMenu is disabled
});

it('styled text is visible, because backButtonDisplayMode is overwritten by backButtonDisplayMode', async () => {
await expectInitialPageToExist('DisabledGenericStyledText', by.text('First'));
await expectBackButtonMenuToNotExistOnLabel('First'); // Check if backButtonMenu is disabled
});
});

// [backButtonMenuEnabled: false and backButtonDisplayMode: minimal]
// backButtonDisplayMode: minimal works as a kill switch so backButtonMenu value is omitted
describe('backButtonDisplayMode: minimal', () => {
it('chevron is used as back button and default text is used in back button menu, backButtonMenuEnabled is omitted', async () => {
await expectInitialPageToExist('DisabledMinimalDefaultText', by.id('chevron.backward'), by.text('First'));
await expectBackButtonMenuIconAndLabel('chevron.backward', 'First'); // Check if backButtonMenu works
});

it('chevron is used as back button and custom text is used in back button menu, backButtonMenuEnabled is omitted', async () => {
await expectInitialPageToExist('DisabledMinimalCustomText', by.id('chevron.backward'), by.text('Custom'));
await expectBackButtonMenuIconAndLabel('chevron.backward', 'Custom'); // Check if backButtonMenu works
});

it('backButtonMenuEnabled is omitted, chevron is used as back button and default text is used in back button menu', async () => {
await expectInitialPageToExist('DisabledMinimalStyledText', by.id('chevron.backward'), by.text('First'));
await expectBackButtonMenuIconAndLabel('chevron.backward', 'First'); // Check if backButtonMenu works
});
});
});

// Custom
it('Default long back label should be truncated to generic by backButtonDisplayMode', async () => {
await expectInitialPageToExist('CustomLongDefaultText', by.text('Back'));
await expectBackButtonMenuWithDifferentLabels('Back', 'LongLongLongLongLong'); // Check if backButtonMenu works
await element(by.text('Pop to top')).tap(); // Go back
});

it('Default label should be truncated to minimal by backButtonDisplayMode when title is long', async () => {
await expectInitialPageToExist('CustomDefaultTextWithLongTitle', by.id('chevron.backward'), by.text('First'));
await expectBackButtonMenuIconAndLabel('chevron.backward', 'First'); // Check if backButtonMenu works
await element(by.text('Pop to top')).tap(); // Go back
});

// TODO: We should be able to fix that
// Custom text overrides backButtonDisplayMode because of using backButtonItem
it.failing('Custom long back label should be truncated to generic by backButtonDisplayMode', async () => {
await expectInitialPageToExist('CustomLongCustomText', by.text('Back'));
await expectBackButtonMenuWithDifferentLabels('Back', 'LongLongLongLongLong'); // Check if backButtonMenu works
await element(by.text('Pop to top')).tap(); // Go back
});

// TODO: We should be able to fix that
// Custom text overrides backButtonDisplayMode because of using backButtonItem
it.failing('Custom back label should be truncated to minimal by backButtonDisplayMode when title is long', async () => {
await expectInitialPageToExist('CustomCustomTextWithLongTitle', by.id('chevron.backward'), by.text('CustomBack'));
await expectBackButtonMenuIconAndLabel('chevron.backward', 'CustomBack'); // Check if backButtonMenu works
await element(by.text('Pop to top')).tap(); // Go back
});
});
48 changes: 48 additions & 0 deletions apps/src/tests/Test2809/Shared.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as React from 'react';
import { Button, Text, View } from 'react-native';
import { ParamListBase } from '@react-navigation/native';
import {
createNativeStackNavigator,
NativeStackNavigationOptions,
NativeStackNavigationProp,
} from '@react-navigation/native-stack';


export function FinalScreen({
navigation,
}: {
navigation: NativeStackNavigationProp<ParamListBase>;
}) {
return (
<View style={{ flex: 1, backgroundColor: 'yellow', justifyContent: 'center', alignItems: 'center' }}>
<Text>VOID</Text>
<Button title="Pop to top" onPress={() => navigation.popTo('Home')} />
</View>
);
}

export function Home({
navigation,
}: {
navigation: NativeStackNavigationProp<ParamListBase>;
}) {
return (
<View style={{ flex: 1, backgroundColor: 'yellow' }}>
<Button
title="Open screen"
onPress={() => navigation.navigate('Second')}
/>
</View>
);
}

export const createStackWithOptions = (options1: NativeStackNavigationOptions, options2: NativeStackNavigationOptions) => () => {
const Stack = createNativeStackNavigator();

return (
<Stack.Navigator>
<Stack.Screen name="First" component={Home} options={options1} />
<Stack.Screen name="Second" component={FinalScreen} options={options2} />
</Stack.Navigator>
);
};
Loading
Loading