Skip to content

Commit 652ce07

Browse files
authored
chore(examples): refactor test structure (#3622)
## Description This PR fixes a minor bug, where previously I could not navigate to screens for scenarios that had their `name !== key`. **HOWEVER**, it's main role is to adjust our approach to test organization. I've had now opportunities to work with the system introduced in <#3528> and I think we can introduce few improvements. Fixes software-mansion/react-native-screens-labs#931 ## Changes First, I stay for now with tests separation for "component integration tests" and "single feature tests", however I change intra-group organization. Instead of describing each test in `index` file, I find it more suitable to place & export manifest from each test module. *In the future we can enrich such manifests, e.g. with detailed test steps / expected outcome description or list or related PRs*. Scenarios are grouped into scenario groups, which have their own name & details that can be displayed in the test app. Second, the single-feature-tests are split into component categories. They had been already, but I think that it was too granular. It does not make sense to test `stack-host` or `stack-screen` separately, as they are tightly coupled. Third, I've introduced single convention for test naming. Tests should be named with *kebab-case*, and the name should reflect the feature being tested. The idea here is that I want to be able to find e.g. all orientation related tests by filtering files with "orientation" in name. Example time: prefer verbose names such as `tests/component-integration-tests/orientation/test-orientation-split-stack.tsx` over `tests/component-integration-tests/orientation/split-stack.tsx`, despite the fact that test scope is contained within directory name. Beside scenario manifest as default export, each test module exports `App` component, which is the root component of the test. From now on this is a required convention. I'll prepare a document summarizing this. ## Visual documentation N/A ## Checklist - [x] Included code example that can be used to test this change. - [x] Updated / created local changelog entries in relevant test files. - [x] For visual changes, included screenshots / GIFs / recordings documenting the change. - [x] For API changes, updated relevant public types. - [x] Ensured that CI passes
1 parent ed43883 commit 652ce07

24 files changed

Lines changed: 216 additions & 161 deletions

File tree

apps/src/tests/component-integration-tests/index.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ import {
55
NavigationIndependentTree,
66
} from '@react-navigation/native';
77
import { createNativeStackNavigator } from '@react-navigation/native-stack';
8-
import { Scenario, splitOnUpperCase } from '../shared/helpers';
8+
import { ScenarioGroup } from '../shared/helpers';
99
import { ScenarioButton } from '../shared/ScenarioButton';
1010

11-
import OrientationScenarios from './orientation';
12-
import ScrollViewScenarios from './scroll-view';
13-
import ScenariosScreen from '../shared/ScenarioScreen';
11+
import OrientationScenarioGroup from './orientation';
12+
import ScrollViewScenarioGroup from './scroll-view';
13+
import ScenarioSelectionScreen from '../shared/ScenarioScreen';
1414

15-
const COMPONENT_SCENARIOS: Record<string, Scenario[]> = {
16-
Orientation: OrientationScenarios,
17-
ScrollView: ScrollViewScenarios,
15+
const COMPONENT_SCENARIOS: Record<string, ScenarioGroup> = {
16+
Orientation: OrientationScenarioGroup,
17+
ScrollView: ScrollViewScenarioGroup,
1818
} as const;
1919

2020
type ParamsList = { [k: keyof typeof COMPONENT_SCENARIOS]: undefined } & {
@@ -24,8 +24,8 @@ type ParamsList = { [k: keyof typeof COMPONENT_SCENARIOS]: undefined } & {
2424
function HomeScreen() {
2525
return (
2626
<ScrollView contentInsetAdjustmentBehavior="automatic">
27-
{Object.entries(COMPONENT_SCENARIOS).map(([key]) => (
28-
<ScenarioButton key={key} title={splitOnUpperCase(key)} route={key} />
27+
{Object.entries(COMPONENT_SCENARIOS).map(([key, scenarioGroup]) => (
28+
<ScenarioButton key={key} title={scenarioGroup.name} route={key} />
2929
))}
3030
</ScrollView>
3131
);
@@ -47,13 +47,12 @@ export default function App() {
4747
headerTitle: 'Scenarios',
4848
}}
4949
/>
50-
{Object.entries(COMPONENT_SCENARIOS).map(([key, scenarios]) => (
50+
{Object.entries(COMPONENT_SCENARIOS).map(([key, scenarioGroup]) => (
5151
<Stack.Screen name={key}>
5252
{() => (
53-
<ScenariosScreen
53+
<ScenarioSelectionScreen
5454
key={key}
55-
title={splitOnUpperCase(key)}
56-
scenarios={scenarios}
55+
scenarioGroup={scenarioGroup}
5756
/>
5857
)}
5958
</Stack.Screen>
Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,12 @@
1-
import { Scenario } from '../../shared/helpers';
2-
import StackInTabs from './StackInTabs';
3-
import TabsInStack from './TabsInStack';
1+
import { ScenarioGroup } from '../../shared/helpers';
2+
import StackInTabs from './orientation-stack-in-tabs';
3+
import TabsInStack from './orientation-tabs-in-stack';
44

5-
const OrientationScenarios: Scenario[] = [
6-
{
7-
name: 'StackInTabs',
8-
details:
9-
'Configuration in Stack contained within TabScreen always takes precedence',
10-
key: 'StackInTabs',
11-
screen: StackInTabs,
12-
platforms: ['ios'],
13-
},
14-
{
15-
name: 'TabsInStack',
16-
details:
17-
'Configuration in Tabs contained within StackScreen should have precedence over configuraton in Stack contained within TabScreen',
18-
key: 'TabsInStack',
19-
screen: TabsInStack,
20-
platforms: ['ios'],
21-
},
22-
];
5+
const OrientationScenarios: ScenarioGroup = {
6+
name: 'Orientation tests',
7+
details:
8+
'Test interaction between different components when orientation changes',
9+
scenarios: [StackInTabs, TabsInStack],
10+
};
2311

2412
export default OrientationScenarios;

apps/src/tests/component-integration-tests/orientation/StackInTabs.tsx renamed to apps/src/tests/component-integration-tests/orientation/orientation-stack-in-tabs.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ import {
1212
createAutoConfiguredTabs,
1313
findTabScreenOptions,
1414
} from '../../shared/tabs';
15+
import { Scenario } from '../../shared/helpers';
16+
17+
const SCENARIO: Scenario = {
18+
name: 'StackInTabs',
19+
details:
20+
'Configuration in Stack contained within TabScreen always takes precedence',
21+
key: 'cit-orientation-stack-in-tabs',
22+
AppComponent: App,
23+
platforms: ['ios'],
24+
};
25+
26+
export default SCENARIO;
1527

1628
type StackParamsList = {
1729
Screen1: undefined;
@@ -84,7 +96,7 @@ const Tabs = createAutoConfiguredTabs<TabsParamsList>({
8496
Tab2: DummyScreen,
8597
});
8698

87-
export default function TabsAndStack() {
99+
export function App() {
88100
return (
89101
<Tabs.Provider>
90102
<Tabs.Autoconfig />

apps/src/tests/component-integration-tests/orientation/TabsInStack.tsx renamed to apps/src/tests/component-integration-tests/orientation/orientation-tabs-in-stack.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ import {
1212
createAutoConfiguredTabs,
1313
findTabScreenOptions,
1414
} from '../../shared/tabs';
15+
import { Scenario } from '../../shared/helpers';
16+
17+
const SCENARIO: Scenario = {
18+
name: 'TabsInStack',
19+
details:
20+
'Configuration in Tabs contained within StackScreen should have precedence over configuraton in Stack contained within TabScreen',
21+
key: 'cit-orientation-tabs-in-stack',
22+
AppComponent: Apps,
23+
platforms: ['ios'],
24+
};
25+
26+
export default SCENARIO;
1527

1628
type StackParamList = {
1729
Screen1: undefined;
@@ -83,7 +95,7 @@ const Stack = createAutoConfiguredStack<StackParamList>({
8395
Screen1: TabsScreen,
8496
});
8597

86-
export default function TabsAndStack() {
98+
export function Apps() {
8799
return (
88100
<Stack.Provider>
89101
<Stack.Autoconfig />
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import { Scenario } from '../../shared/helpers';
1+
import { ScenarioGroup } from '../../shared/helpers';
22

3-
const ScrollViewScenarios: Scenario[] = [];
3+
const ScrollViewScenarioGroup: ScenarioGroup = {
4+
name: 'ScrollView integration tests',
5+
details: 'Tests related to integration of our components with ScrollView',
6+
scenarios: [],
7+
};
48

5-
export default ScrollViewScenarios;
9+
export default ScrollViewScenarioGroup;

apps/src/tests/shared/ScenarioScreen.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useMemo } from 'react';
22
import { ScrollView } from 'react-native';
3-
import { Scenario } from './helpers';
3+
import { Scenario, ScenarioGroup } from './helpers';
44
import { ScenarioButton } from './ScenarioButton';
55
import { createNativeStackNavigator } from '@react-navigation/native-stack';
66
import {
@@ -24,9 +24,8 @@ function ScenarioSelect(props: { scenarios: Scenario[] }) {
2424
);
2525
}
2626

27-
export default function ScenariosScreen(props: {
28-
title: string;
29-
scenarios: Scenario[];
27+
export default function ScenarioSelectionScreen(props: {
28+
scenarioGroup: ScenarioGroup;
3029
}) {
3130
const Stack = useMemo(() => createNativeStackNavigator(), []);
3231

@@ -40,12 +39,12 @@ export default function ScenariosScreen(props: {
4039
options={{
4140
headerShown: true,
4241
headerLargeTitleEnabled: true,
43-
headerTitle: props.title,
42+
headerTitle: props.scenarioGroup.name,
4443
}}>
45-
{() => <ScenarioSelect scenarios={props.scenarios} />}
44+
{() => <ScenarioSelect scenarios={props.scenarioGroup.scenarios} />}
4645
</Stack.Screen>
47-
{props.scenarios.map(({ name, key, screen }) => (
48-
<Stack.Screen name={name} key={key} component={screen} />
46+
{props.scenarioGroup.scenarios.map(({ key, AppComponent }) => (
47+
<Stack.Screen name={key} key={key} component={AppComponent} />
4948
))}
5049
</Stack.Navigator>
5150
</NavigationContainer>

apps/src/tests/shared/helpers.ts

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,43 @@
1+
/**
2+
* Models single test scenario.
3+
*/
14
export interface Scenario {
5+
/**
6+
* Human readable name of the scenario. White spaces allowed.
7+
*/
28
name: string;
9+
/**
10+
* Globally unique key identifying this scenario.
11+
* Must be in kebab-case.
12+
* Should match the filename of the scenario file.
13+
*/
314
key: string;
15+
/**
16+
* Additional description of what this test covers.
17+
*/
418
details?: string;
19+
/**
20+
* What platforms does this test cover.
21+
*/
522
platforms?: ('android' | 'ios')[];
6-
screen: React.ComponentType;
23+
/**
24+
* Component that will render the test scenario. It should be standalone!
25+
* That means it should be possible to render this w/o any additional harness
26+
* as top-level application component & it should remain functional.
27+
*/
28+
AppComponent: React.ComponentType;
729
}
830

9-
export type KeyList = Record<keyof any, undefined>;
10-
11-
const UC_REGEX = /[A-Z][^A-Z]+/g;
12-
13-
/**
14-
* Splits a string into words based on uppercase letters
15-
*/
16-
export function splitOnUpperCase(str: string) {
17-
if (str.length === 0) {
18-
return '';
19-
}
20-
21-
const matches = [...str.matchAll(UC_REGEX)];
22-
23-
return matches.map(m => m.at(0) ?? '').join(' ');
31+
export interface ScenarioGroup {
32+
/**
33+
* Name of this scenario group
34+
*/
35+
name: string;
36+
/**
37+
* Additional description of what this group of scenarios is related to.
38+
*/
39+
details?: string;
40+
scenarios: Scenario[];
2441
}
42+
43+
export type KeyList = Record<keyof any, undefined>;

apps/src/tests/single-feature-tests/index.tsx

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,19 @@ import {
66
} from '@react-navigation/native';
77
import { createNativeStackNavigator } from '@react-navigation/native-stack';
88

9-
import BottomTabsScenarios from './tabs-host';
10-
import BottomTabsScreenScenarios from './tabs-screen';
11-
import SplitHostScenarios from './split-host';
12-
import SplitScreenScenarios from './split-screen';
13-
import StackHostScenarios from './stack-host';
14-
import StackScreenScenarios from './stack-screen';
15-
import { Scenario, splitOnUpperCase } from '../shared/helpers';
9+
import TabsScenarioGroup from './tabs';
10+
import SplitScenarioGroup from './split';
11+
import StackV5ScenarioGroup from './stack-v5';
12+
import StackV4ScenarioGroup from './stack-v4';
13+
import type { ScenarioGroup } from '../shared/helpers';
1614
import { ScenarioButton } from '../shared/ScenarioButton';
17-
import ScenariosScreen from '../shared/ScenarioScreen';
15+
import ScenarioSelectionScreen from '../shared/ScenarioScreen';
1816

19-
export const COMPONENT_SCENARIOS: Record<string, Scenario[]> = {
20-
BottomTabs: BottomTabsScenarios,
21-
BottomTabsScreen: BottomTabsScreenScenarios,
22-
SplitHost: SplitHostScenarios,
23-
SplitScreen: SplitScreenScenarios,
24-
StackHost: StackHostScenarios,
25-
StackScreen: StackScreenScenarios,
17+
export const COMPONENT_SCENARIOS: Record<string, ScenarioGroup> = {
18+
Tabs: TabsScenarioGroup,
19+
Split: SplitScenarioGroup,
20+
StackV5: StackV5ScenarioGroup,
21+
StackV4: StackV4ScenarioGroup,
2622
} as const;
2723

2824
type ParamsList = { [k: keyof typeof COMPONENT_SCENARIOS]: undefined } & {
@@ -32,8 +28,13 @@ type ParamsList = { [k: keyof typeof COMPONENT_SCENARIOS]: undefined } & {
3228
function HomeScreen() {
3329
return (
3430
<ScrollView contentInsetAdjustmentBehavior="automatic">
35-
{Object.entries(COMPONENT_SCENARIOS).map(([key]) => (
36-
<ScenarioButton key={key} title={splitOnUpperCase(key)} route={key} />
31+
{Object.entries(COMPONENT_SCENARIOS).map(([key, scenarioGroup]) => (
32+
<ScenarioButton
33+
key={key}
34+
title={scenarioGroup.name}
35+
route={key}
36+
details={scenarioGroup.details}
37+
/>
3738
))}
3839
</ScrollView>
3940
);
@@ -55,13 +56,12 @@ export default function App() {
5556
headerTitle: 'Scenarios',
5657
}}
5758
/>
58-
{Object.entries(COMPONENT_SCENARIOS).map(([key, scenarios]) => (
59+
{Object.entries(COMPONENT_SCENARIOS).map(([key, scenarioGroup]) => (
5960
<Stack.Screen name={key}>
6061
{() => (
61-
<ScenariosScreen
62+
<ScenarioSelectionScreen
6263
key={key}
63-
title={splitOnUpperCase(key)}
64-
scenarios={scenarios}
64+
scenarioGroup={scenarioGroup}
6565
/>
6666
)}
6767
</Stack.Screen>

apps/src/tests/single-feature-tests/split-host/index.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

apps/src/tests/single-feature-tests/split-screen/index.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)