Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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: 2 additions & 0 deletions apps/src/tests/single-feature-tests/tabs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import TestTabsPreventNativeSelection from './test-tabs-prevent-native-selection
import TestTabsStaleStateUpdateRejection from './test-tabs-stale-update-rejection';
import TestTabsTabBarMinimizeBehavior from './test-tabs-tab-bar-minimize-behavior-ios';
import TestTabsTabBarControllerMode from './test-tabs-tab-bar-controller-mode-ios';
import testTabsEvents from './test-tabs-events';

const scenarios = {
BottomAccessoryScenario,
Expand All @@ -30,6 +31,7 @@ const scenarios = {
TestTabsStaleStateUpdateRejection,
TestTabsTabBarMinimizeBehavior,
TestTabsTabBarControllerMode,
testTabsEvents,
};

const TabsScenarioGroup: ScenarioGroup<keyof typeof scenarios> = {
Expand Down
119 changes: 119 additions & 0 deletions apps/src/tests/single-feature-tests/tabs/test-tabs-events/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { useCallback } from 'react';
import { StyleSheet, Text } from 'react-native';
import type { ScenarioDescription } from '@apps/tests/shared/helpers';
import { createScenario } from '@apps/tests/shared/helpers';
import {
TabsContainer,
type TabRouteConfig,
DEFAULT_TAB_ROUTE_OPTIONS,
useTabsNavigationContext,
} from '@apps/shared/gamma/containers/tabs';
import { CenteredLayoutView } from '@apps/shared/CenteredLayoutView';
import { ToastProvider, useToast } from '@apps/shared/';
import { Colors } from '@apps/shared/styling';

const scenarioDescription: ScenarioDescription = {
name: 'Tabs lifecycle events',
key: 'test-tabs-events',
details:
'Verifies onWillAppear, onDidAppear, onWillDisappear, onDidDisappear fire in the correct order when switching tabs.',
platforms: ['ios', 'android'],
};

function TabScreen() {
const { routeKey } = useTabsNavigationContext();

return (
<CenteredLayoutView testID={`tabContent-${routeKey}`}>
<Text style={styles.tabLabel} testID={`tabLabel-${routeKey}`}>
{routeKey}
</Text>
<Text style={styles.tabHint}>Switch tabs to trigger lifecycle events</Text>
</CenteredLayoutView>
);
}

function AppContents() {
const toast = useToast();

const makeCallbacks = useCallback(
(tabName: string) => ({
onWillAppear: () =>
toast.push({
message: `${tabName}: onWillAppear`,
backgroundColor: Colors.GreenLight100,
}),
onDidAppear: () =>
toast.push({
message: `${tabName}: onDidAppear`,
backgroundColor: Colors.BlueLight100,
}),
onWillDisappear: () =>
toast.push({
message: `${tabName}: onWillDisappear`,
backgroundColor: Colors.NavyLight60,
}),
onDidDisappear: () =>
toast.push({
message: `${tabName}: onDidDisappear`,
backgroundColor: Colors.NavyLight100,
}),
}),
[toast],
);

const TAB_CONFIGS: TabRouteConfig[] = [
{
name: 'TabA',
Component: TabScreen,
options: {
...DEFAULT_TAB_ROUTE_OPTIONS,
title: 'Tab A',
...makeCallbacks('TabA'),
},
},
{
name: 'TabB',
Component: TabScreen,
options: {
...DEFAULT_TAB_ROUTE_OPTIONS,
title: 'Tab B',
...makeCallbacks('TabB'),
},
},
{
name: 'TabC',
Component: TabScreen,
options: {
...DEFAULT_TAB_ROUTE_OPTIONS,
title: 'Tab C',
...makeCallbacks('TabC'),
},
},
];
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

TAB_CONFIGS is recreated on every render of AppContents(). In this codebase other tab scenarios typically keep ROUTE_CONFIGS/TAB_CONFIGS stable (module-level const or useMemo), which avoids unnecessary work in TabsContainer (including useSanitizeRouteConfigs) and makes the component tree less noisy during re-renders (e.g., when a toast is pushed).

Copilot uses AI. Check for mistakes.

return <TabsContainer routeConfigs={TAB_CONFIGS} />;
}

export function App() {
return (
<ToastProvider>
<AppContents />
</ToastProvider>
);
}

export default createScenario(App, scenarioDescription);

const styles = StyleSheet.create({
tabLabel: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 8,
},
tabHint: {
color: '#666',
fontSize: 13,
textAlign: 'center',
},
});
121 changes: 121 additions & 0 deletions apps/src/tests/single-feature-tests/tabs/test-tabs-events/scenario.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Test Scenario: Tabs lifecycle events

## Details

**Description:** Verifies that `onWillAppear`, `onDidAppear`,
`onWillDisappear`, and `onDidDisappear` fire in the correct order on tab
switches, covering happy-path transitions, re-tapping the active tab, and
rapid switching.

**OS test creation version:** iOS: 18.6 and 26.2, Android: 18.6.

## E2E test

Other: On going research
Comment thread
LKuchno marked this conversation as resolved.
Outdated

## Prerequisites

- iOS simulator or device (iPhone)
- Android emulator or device

## Note

- All four events should fire on every tab switch. The expected order for
a switch between TabX and TabY depends on platform
For iOS:
1. `TabY: onWillAppear`
2. `TabX: onWillDisappear`
3. `TabY: onDidAppear`
4. `TabX: onDidDisappear`
For Android:
1. `TabX: onWillDisappear`
2. `TabX: onDidDisappear`
3. `TabY: onWillAppear`
4. `TabY: onDidAppear`
- Toasts stack and dismiss automatically; observe each toast color and
label as it appears. To dismiss a toast manually, tap it.
- Re-tapping the currently active tab must not fire any lifecycle events.

## Steps

### Baseline

1. Launch the app and navigate to **Tabs lifecycle events**.

- [ ] Expected: Three tabs are visible in the tab bar: **Tab A**, **Tab B**,
and **Tab C**. **Tab A** is selected. Two toasts
appear for the initial Tab A appearance:
- `TabA: onWillAppear`
- `TabA: onDidAppear`

---

### Tab A → Tab B transition

2. Tap **Tab B** in the tab bar.

- [ ] Expected: The content area switches to show "TabB". Four toast
notifications appear in order specific platform:
- `TabB: onWillAppear` (green background)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

Minor grammar: "in order specific platform" is ungrammatical; consider changing to something like "in a platform-specific order" (and apply the same fix in the other transition sections where this phrase is repeated).

Copilot uses AI. Check for mistakes.
- `TabA: onWillDisappear` (light navy background)
- `TabB: onDidAppear` (light blue background)
- `TabA: onDidDisappear` (dark navy background)

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The per-transition expected toast order is described as platform-specific, but the listed sequences here match only the iOS order. On Android the expected ordering differs (per the Note section), so the Steps section should either split expected results into iOS vs Android sequences or refer back to the platform-specific order instead of listing a single sequence.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

---

### Tab B → Tab C transition

3. Tap **Tab C** in the tab bar.

- [ ] Expected: The content area switches to show "TabC". Four toast
notifications appear in order specific platform:
- `TabC: onWillAppear` (green background)
- `TabB: onWillDisappear` (light navy background)
- `TabC: onDidAppear` (light blue background)
- `TabB: onDidDisappear` (dark navy background)

---

### Tab C → Tab A transition

4. Tap **Tab A** in the tab bar.

- [ ] Expected: The content area switches to show "TabA". Four toast
notifications appear in order specific platform:
- `TabA: onWillAppear` (green background)
- `TabC: onWillDisappear` (light navy background)
- `TabA: onDidAppear` (light blue background)
- `TabC: onDidDisappear` (dark navy background)

---

### Re-tapping the active tab (edge case)

5. With **Tab A** selected, tap **Tab A** again in the tab bar.

- [ ] Expected: The content area does not change. No toast notifications
appear. No lifecycle events fire for a tap on the already-active tab.

---

### Rapid tab switching (edge case)

6. Tap **Tab B**, then immediately tap **Tab C** before the toasts from
the previous step have finished dismissing.

- [ ] Expected: Both transitions complete. Toasts from the B→C transition
appear after the A→B toasts. The final selected
tab is **Tab C** and its content area shows "TabC". No events are
missing or duplicated — all eight toasts from both transitions are
eventually shown.

---

### Full round-trip verification

7. From **Tab C**, tap **Tab A**, then **Tab B**, then **Tab C**.

- [ ] Expected: Each tab switch produces exactly four toasts (will/did
disappear for the leaving tab, will/did appear for the arriving tab).
After three switches, twelve toasts in total have been fired. The final
selected tab is **Tab C**.
Loading