From 7b99a2a9abeeae29f946f0555d7e4efab7b15103 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 6 May 2026 16:11:01 +0300 Subject: [PATCH 1/3] feat: add shared jest tests for demo apps --- CONTRIBUTING.md | 27 +- .../__tests__/brownfield.example.test.tsx | 12 + apps/ExpoApp54/jest.config.js | 30 + apps/ExpoApp54/jest.setup.js | 1 + apps/ExpoApp54/package.json | 9 +- .../__tests__/brownfield.example.test.tsx | 12 + apps/ExpoApp55/eslint.config.js | 8 + apps/ExpoApp55/jest.config.js | 33 + apps/ExpoApp55/jest.setup.js | 1 + apps/ExpoApp55/jest/cssMock.js | 2 + apps/ExpoApp55/package.json | 9 +- apps/RNApp/__tests__/App.test.tsx | 3 +- .../__tests__/brownfield.example.test.tsx | 12 + apps/RNApp/jest.config.js | 25 + apps/RNApp/jest.setup.js | 1 + apps/RNApp/package.json | 4 +- .../jest/setup.js | 56 + .../package.json | 28 + .../src/global.d.ts | 7 + .../src/index.ts | 4 + .../src/suites/counter.suite.tsx | 35 + .../src/suites/expoRnApp.suite.tsx | 51 + .../src/suites/homeScreen.suite.tsx | 116 ++ .../src/suites/postMessageTab.suite.tsx | 70 + .../tsconfig.json | 24 + package.json | 1 + packages/brownie/package.json | 1 + packages/cli/package.json | 3 +- packages/cli/turbo.json | 3 + turbo.json | 4 +- yarn.lock | 1388 ++++++++++++++++- 31 files changed, 1933 insertions(+), 47 deletions(-) create mode 100644 apps/ExpoApp54/__tests__/brownfield.example.test.tsx create mode 100644 apps/ExpoApp54/jest.config.js create mode 100644 apps/ExpoApp54/jest.setup.js create mode 100644 apps/ExpoApp55/__tests__/brownfield.example.test.tsx create mode 100644 apps/ExpoApp55/jest.config.js create mode 100644 apps/ExpoApp55/jest.setup.js create mode 100644 apps/ExpoApp55/jest/cssMock.js create mode 100644 apps/RNApp/__tests__/brownfield.example.test.tsx create mode 100644 apps/RNApp/jest.setup.js create mode 100644 apps/brownfield-example-shared-tests/jest/setup.js create mode 100644 apps/brownfield-example-shared-tests/package.json create mode 100644 apps/brownfield-example-shared-tests/src/global.d.ts create mode 100644 apps/brownfield-example-shared-tests/src/index.ts create mode 100644 apps/brownfield-example-shared-tests/src/suites/counter.suite.tsx create mode 100644 apps/brownfield-example-shared-tests/src/suites/expoRnApp.suite.tsx create mode 100644 apps/brownfield-example-shared-tests/src/suites/homeScreen.suite.tsx create mode 100644 apps/brownfield-example-shared-tests/src/suites/postMessageTab.suite.tsx create mode 100644 apps/brownfield-example-shared-tests/tsconfig.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6111f37..4b63cbfb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,6 +19,7 @@ We use [changesets](https://github.com/changesets/changesets) to make it easier - `lint` - runs linting on all JS/TS source files in the monorepo _[Turbo]_ - `gradle-plugin:lint` - runs linting on the Brownfield Gradle plugin source code - `typecheck` - runs TypeScript type checking on all TS source files in the monorepo _[Turbo]_ +- `test:apps` - runs Jest for the React Native example apps under `apps/` (Expo 54, Expo 55, plain RN) _[Turbo]_ - `build` - runs all `build*` tasks in the Turbo repo - see below for more details _[Turbo]_ - `dev` - runs all `dev` tasks in all workspaces - `brownfield:plugin:publish:local` - publishes the Brownfield Gradle plugin to your local Maven repository for testing purposes @@ -27,8 +28,30 @@ We use [changesets](https://github.com/changesets/changesets) to make it easier - `build:example:android-rn` - builds the example React Native app for Android (`apps/RNApp/android`) - `build:example:ios-rn` - builds the example React Native app for iOS (`apps/RNApp/ios`) - `build:example:android-consumer:expo55` - builds the example native Android consumer (`apps/AndroidApp`) app's flavor consuming the Expo 55 RN app (`apps/ExpoApp55`) artifact -- - `build:example:android-consumer:expo54` - builds the example native Android consumer (`apps/AndroidApp`) app's flavor consuming the Expo 54 RN app (`apps/ExpoApp54`) artifact -- - `build:example:android-consumer:vanilla` - builds the example native Android consumer (`apps/AndroidApp`) app's flavor consuming the vanilla RN app (`apps/RNApp`) artifact +- `build:example:android-consumer:expo54` - builds the example native Android consumer (`apps/AndroidApp`) app's flavor consuming the Expo 54 RN app (`apps/ExpoApp54`) artifact +- `build:example:android-consumer:vanilla` - builds the example native Android consumer (`apps/AndroidApp`) app's flavor consuming the vanilla RN app (`apps/RNApp`) artifact - `build:example:ios-consumer:expo55` - builds the example native iOS consumer app (`apps/AppleApp`) consuming the Expo 55 RN app (`apps/ExpoApp55`) artifact - `build:example:ios-consumer:expo54` - builds the example native iOS consumer app (`apps/AppleApp`) consuming the Expo 54 RN app (`apps/ExpoApp54`) artifact - `build:example:ios-consumer:vanilla` - builds the example native iOS consumer (`apps/AppleApp`) app's flavor consuming the vanilla RN app (`apps/RNApp`) artifact + +## Example app tests + +The React Native example apps share Jest utilities and test suites from `apps/brownfield-example-shared-tests`. Tests exercise integration with `@callstack/react-native-brownfield`, `@callstack/brownfield-navigation`, and `@callstack/brownie` as used in each demo. + +From the repository root: + +| Command | Description | +| --- | --- | +| `yarn test:apps` | Runs `test` in all workspaces under `apps/` that define it (via Turbo). | + +Per example app (run from the repo root): + +| Command | App | +| --- | --- | +| `yarn workspace @callstack/brownfield-example-rn-app test` | Plain React Native (`apps/RNApp`) | +| `yarn workspace @callstack/brownfield-example-expo-app-54 test` | Expo SDK 54 (`apps/ExpoApp54`) | +| `yarn workspace @callstack/brownfield-example-expo-app-55 test` | Expo SDK 55 (`apps/ExpoApp55`) | + +Package-level scripts (`yarn test` inside `apps/RNApp`, `apps/ExpoApp54`, or `apps/ExpoApp55`) invoke Jest with each app’s `jest.config.js`. + +The native-only sample apps (`apps/AppleApp`, `apps/AndroidApp`) use their platform test runners (Xcode / Gradle), not Jest. diff --git a/apps/ExpoApp54/__tests__/brownfield.example.test.tsx b/apps/ExpoApp54/__tests__/brownfield.example.test.tsx new file mode 100644 index 00000000..e5309813 --- /dev/null +++ b/apps/ExpoApp54/__tests__/brownfield.example.test.tsx @@ -0,0 +1,12 @@ +import PostMessageTab from '../app/(tabs)/postMessage'; +import Counter from '../components/counter'; +import RNApp from '../RNApp'; +import { + runPostMessageTabSuite, + runCounterSuite, + runExpoRnAppSuite, +} from '@callstack/brownfield-example-shared-tests'; + +runPostMessageTabSuite('ExpoApp54', PostMessageTab); +runCounterSuite('ExpoApp54', Counter); +runExpoRnAppSuite('ExpoApp54', RNApp); diff --git a/apps/ExpoApp54/jest.config.js b/apps/ExpoApp54/jest.config.js new file mode 100644 index 00000000..4d40cb80 --- /dev/null +++ b/apps/ExpoApp54/jest.config.js @@ -0,0 +1,30 @@ +const path = require('path'); + +module.exports = { + preset: 'jest-expo', + setupFilesAfterEnv: ['/jest.setup.js'], + moduleNameMapper: { + '^react$': require.resolve('react'), + '^react/jsx-runtime$': require.resolve('react/jsx-runtime'), + '^react/jsx-dev-runtime$': require.resolve('react/jsx-dev-runtime'), + '^@testing-library/react-native$': require.resolve( + '@testing-library/react-native' + ), + '^@/(.*)$': '/$1', + '^@callstack/react-native-brownfield$': path.join( + __dirname, + '../../packages/react-native-brownfield/src/index.ts' + ), + '^@callstack/brownfield-navigation$': path.join( + __dirname, + '../../packages/brownfield-navigation/src/index.ts' + ), + '^@callstack/brownie$': path.join( + __dirname, + '../../packages/brownie/src/index.ts' + ), + }, + transformIgnorePatterns: [ + 'node_modules/(?!((jest-)?react-native|@react-native(-community)?|@callstack/brownfield-example-shared-tests|expo|@expo|expo-modules-core)/)', + ], +}; diff --git a/apps/ExpoApp54/jest.setup.js b/apps/ExpoApp54/jest.setup.js new file mode 100644 index 00000000..14d2c259 --- /dev/null +++ b/apps/ExpoApp54/jest.setup.js @@ -0,0 +1 @@ +require('@callstack/brownfield-example-shared-tests/jest/setup'); diff --git a/apps/ExpoApp54/package.json b/apps/ExpoApp54/package.json index 0a85eaf7..3b1523d7 100644 --- a/apps/ExpoApp54/package.json +++ b/apps/ExpoApp54/package.json @@ -8,7 +8,8 @@ "android": "expo run:android", "ios": "expo run:ios", "web": "expo start --web", - "lint": "expo lint", + "lint": "expo lint --no-cache", + "test": "jest --config jest.config.js", "prebuild": "expo prebuild", "brownfield:prepare:android:ci": "cd .. && node --experimental-strip-types --no-warnings ./scripts/prepare-android-build-gradle-for-ci.ts ExpoApp54", "brownfield:package:android": "brownfield package:android --module-name brownfieldlib --variant release", @@ -41,9 +42,15 @@ "react-native-worklets": "0.5.1" }, "devDependencies": { + "@callstack/brownfield-example-shared-tests": "workspace:^", + "@testing-library/react-native": "^13.3.3", + "@types/jest": "^30.0.0", "@types/react": "~19.1.10", "eslint": "^9.25.0", "eslint-config-expo": "~10.0.0", + "jest": "^29.7.0", + "jest-expo": "~54.0.16", + "react-test-renderer": "19.1.0", "typescript": "~5.9.3" }, "brownie": { diff --git a/apps/ExpoApp55/__tests__/brownfield.example.test.tsx b/apps/ExpoApp55/__tests__/brownfield.example.test.tsx new file mode 100644 index 00000000..eeaf54e0 --- /dev/null +++ b/apps/ExpoApp55/__tests__/brownfield.example.test.tsx @@ -0,0 +1,12 @@ +import PostMessageTab from '../src/app/postMessage'; +import Counter from '../src/components/counter'; +import RNApp from '../RNApp'; +import { + runPostMessageTabSuite, + runCounterSuite, + runExpoRnAppSuite, +} from '@callstack/brownfield-example-shared-tests'; + +runPostMessageTabSuite('ExpoApp55', PostMessageTab); +runCounterSuite('ExpoApp55', Counter); +runExpoRnAppSuite('ExpoApp55', RNApp); diff --git a/apps/ExpoApp55/eslint.config.js b/apps/ExpoApp55/eslint.config.js index 5025da68..a852d76e 100644 --- a/apps/ExpoApp55/eslint.config.js +++ b/apps/ExpoApp55/eslint.config.js @@ -1,10 +1,18 @@ // https://docs.expo.dev/guides/using-eslint/ const { defineConfig } = require('eslint/config'); const expoConfig = require('eslint-config-expo/flat'); +const globals = require('globals'); module.exports = defineConfig([ expoConfig, { ignores: ['dist/*'], }, + // Jest config is executed by Node (CommonJS); teach ESLint Node globals like __dirname. + { + files: ['jest.config.js'], + languageOptions: { + globals: globals.node, + }, + }, ]); diff --git a/apps/ExpoApp55/jest.config.js b/apps/ExpoApp55/jest.config.js new file mode 100644 index 00000000..b660b8b4 --- /dev/null +++ b/apps/ExpoApp55/jest.config.js @@ -0,0 +1,33 @@ +const path = require('path'); + +module.exports = { + preset: 'jest-expo', + setupFilesAfterEnv: ['/jest.setup.js'], + moduleNameMapper: { + '^react$': require.resolve('react'), + '^react/jsx-runtime$': require.resolve('react/jsx-runtime'), + '^react/jsx-dev-runtime$': require.resolve('react/jsx-dev-runtime'), + '^@testing-library/react-native$': require.resolve( + '@testing-library/react-native' + ), + // Match before `@/` so `@/global.css` is stubbed (Expo web styling). + '^.+\\.css$': '/jest/cssMock.js', + '^@/(.*)$': '/src/$1', + '^@/assets/(.*)$': '/assets/$1', + '^@callstack/react-native-brownfield$': path.join( + __dirname, + '../../packages/react-native-brownfield/src/index.ts' + ), + '^@callstack/brownfield-navigation$': path.join( + __dirname, + '../../packages/brownfield-navigation/src/index.ts' + ), + '^@callstack/brownie$': path.join( + __dirname, + '../../packages/brownie/src/index.ts' + ), + }, + transformIgnorePatterns: [ + 'node_modules/(?!((jest-)?react-native|@react-native(-community)?|@callstack/brownfield-example-shared-tests|expo|@expo|expo-modules-core)/)', + ], +}; diff --git a/apps/ExpoApp55/jest.setup.js b/apps/ExpoApp55/jest.setup.js new file mode 100644 index 00000000..14d2c259 --- /dev/null +++ b/apps/ExpoApp55/jest.setup.js @@ -0,0 +1 @@ +require('@callstack/brownfield-example-shared-tests/jest/setup'); diff --git a/apps/ExpoApp55/jest/cssMock.js b/apps/ExpoApp55/jest/cssMock.js new file mode 100644 index 00000000..815ec05b --- /dev/null +++ b/apps/ExpoApp55/jest/cssMock.js @@ -0,0 +1,2 @@ +/** Stub CSS imports for Jest (used by Expo Router / bundler pipeline). */ +module.exports = {}; diff --git a/apps/ExpoApp55/package.json b/apps/ExpoApp55/package.json index 4dfb63e1..1c1e17fa 100644 --- a/apps/ExpoApp55/package.json +++ b/apps/ExpoApp55/package.json @@ -7,7 +7,8 @@ "android": "expo run:android", "ios": "expo run:ios", "web": "expo start --web", - "lint": "expo lint", + "lint": "expo lint --no-cache", + "test": "jest --config jest.config.js", "prebuild": "expo prebuild", "brownfield:prepare:android:ci": "cd .. && node --experimental-strip-types --no-warnings ./scripts/prepare-android-build-gradle-for-ci.ts ExpoApp55", "brownfield:package:android": "brownfield package:android --module-name brownfieldlib --variant release", @@ -46,9 +47,15 @@ "react-native-worklets": "0.7.2" }, "devDependencies": { + "@callstack/brownfield-example-shared-tests": "workspace:^", + "@testing-library/react-native": "^13.3.3", + "@types/jest": "^30.0.0", "@types/react": "~19.2.10", "eslint": "^9.25.0", "eslint-config-expo": "~55.0.0", + "jest": "^29.7.0", + "jest-expo": "~55.0.0", + "react-test-renderer": "19.2.0", "typescript": "~5.9.2" }, "private": true, diff --git a/apps/RNApp/__tests__/App.test.tsx b/apps/RNApp/__tests__/App.test.tsx index e532f701..f6ad9fa1 100644 --- a/apps/RNApp/__tests__/App.test.tsx +++ b/apps/RNApp/__tests__/App.test.tsx @@ -2,9 +2,8 @@ * @format */ -import React from 'react'; import ReactTestRenderer from 'react-test-renderer'; -import App from '../App'; +import App from '../src/App'; test('renders correctly', async () => { await ReactTestRenderer.act(() => { diff --git a/apps/RNApp/__tests__/brownfield.example.test.tsx b/apps/RNApp/__tests__/brownfield.example.test.tsx new file mode 100644 index 00000000..5ce7dcfc --- /dev/null +++ b/apps/RNApp/__tests__/brownfield.example.test.tsx @@ -0,0 +1,12 @@ +import { + runCounterSuite, + runHomeScreenSuite, +} from '@callstack/brownfield-example-shared-tests'; +import { HomeScreen } from '../src/HomeScreen'; +import Counter from '../src/components/counter'; +import { getRandomTheme } from '../src/utils'; + +const theme = getRandomTheme(); + +runHomeScreenSuite('RNApp', HomeScreen); +runCounterSuite('RNApp', Counter, { colors: theme }); diff --git a/apps/RNApp/jest.config.js b/apps/RNApp/jest.config.js index 294be30f..bb08fc51 100644 --- a/apps/RNApp/jest.config.js +++ b/apps/RNApp/jest.config.js @@ -1,3 +1,28 @@ +const path = require('node:path'); + module.exports = { preset: '@react-native/jest-preset', + setupFilesAfterEnv: ['/jest.setup.js'], + moduleNameMapper: { + '^react$': require.resolve('react'), + '^react/jsx-runtime$': require.resolve('react/jsx-runtime'), + '^react/jsx-dev-runtime$': require.resolve('react/jsx-dev-runtime'), + '^@testing-library/react-native$': + require.resolve('@testing-library/react-native'), + '^@callstack/react-native-brownfield$': path.join( + __dirname, + '../../packages/react-native-brownfield/src/index.ts' + ), + '^@callstack/brownfield-navigation$': path.join( + __dirname, + '../../packages/brownfield-navigation/src/index.ts' + ), + '^@callstack/brownie$': path.join( + __dirname, + '../../packages/brownie/src/index.ts' + ), + }, + transformIgnorePatterns: [ + 'node_modules/(?!((jest-)?react-native|@react-native(-community)?|@callstack/brownfield-example-shared-tests|@react-navigation|react-native-screens|react-native-safe-area-context)/)', + ], }; diff --git a/apps/RNApp/jest.setup.js b/apps/RNApp/jest.setup.js new file mode 100644 index 00000000..14d2c259 --- /dev/null +++ b/apps/RNApp/jest.setup.js @@ -0,0 +1 @@ +require('@callstack/brownfield-example-shared-tests/jest/setup'); diff --git a/apps/RNApp/package.json b/apps/RNApp/package.json index 8ec3f40e..99cfee27 100644 --- a/apps/RNApp/package.json +++ b/apps/RNApp/package.json @@ -12,7 +12,7 @@ "brownfield:package:ios": "brownfield package:ios --scheme BrownfieldLib --configuration Release", "lint": "eslint .", "start": "react-native start", - "test": "jest", + "test": "jest --config jest.config.js", "codegen": "brownfield codegen", "codegen:navigation": "brownfield navigation:codegen brownfield.navigation.ts" }, @@ -31,6 +31,7 @@ "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.25.0", + "@callstack/brownfield-example-shared-tests": "workspace:^", "@react-native-community/cli": "20.1.0", "@react-native-community/cli-platform-android": "20.1.0", "@react-native-community/cli-platform-ios": "20.1.0", @@ -39,6 +40,7 @@ "@react-native/jest-preset": "0.85.0", "@react-native/metro-config": "0.85.0", "@react-native/typescript-config": "0.85.0", + "@testing-library/react-native": "^13.3.3", "@types/jest": "^30.0.0", "@types/react": "^19.2.0", "@types/react-test-renderer": "^19.1.0", diff --git a/apps/brownfield-example-shared-tests/jest/setup.js b/apps/brownfield-example-shared-tests/jest/setup.js new file mode 100644 index 00000000..8ae248f7 --- /dev/null +++ b/apps/brownfield-example-shared-tests/jest/setup.js @@ -0,0 +1,56 @@ +/** + * Shared Jest setup for brownfield example apps. Include via setupFilesAfterEnv. + * + * @see https://jestjs.io/docs/configuration#setupfilesafterenv-array + */ + +global.__brownieExampleCounter = 0; +global.__resetBrownieExampleStore = () => { + global.__brownieExampleCounter = 0; +}; + +jest.mock('@callstack/react-native-brownfield', () => ({ + __esModule: true, + default: { + onMessage: jest.fn(() => ({ remove: jest.fn() })), + postMessage: jest.fn(), + setNativeBackGestureAndButtonEnabled: jest.fn(), + popToNative: jest.fn(), + }, +})); + +jest.mock('@callstack/brownfield-navigation', () => ({ + __esModule: true, + default: { + navigateToSettings: jest.fn(), + navigateToReferrals: jest.fn(), + }, +})); + +jest.mock('@callstack/brownie', () => ({ + __esModule: true, + /** Resolve React from the app running Jest (cwd), not from this package's nested deps. */ + useStore(key, selector) { + const React = require(require.resolve('react', { paths: [process.cwd()] })); + const [, setRev] = React.useState(0); + const setState = React.useCallback( + (action) => { + const prev = { counter: global.__brownieExampleCounter }; + const partial = typeof action === 'function' ? action(prev) : action; + if ( + partial && + typeof partial === 'object' && + partial.counter !== undefined + ) { + global.__brownieExampleCounter = partial.counter; + } + setRev((r) => r + 1); + }, + [selector] + ); + return [ + selector({ counter: global.__brownieExampleCounter }), + setState, + ]; + }, +})); diff --git a/apps/brownfield-example-shared-tests/package.json b/apps/brownfield-example-shared-tests/package.json new file mode 100644 index 00000000..03c23824 --- /dev/null +++ b/apps/brownfield-example-shared-tests/package.json @@ -0,0 +1,28 @@ +{ + "name": "@callstack/brownfield-example-shared-tests", + "version": "0.0.1", + "private": true, + "scripts": { + "test": "node -e \"process.exit(0)\"" + }, + "main": "src/index.ts", + "types": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./jest/setup": "./jest/setup.js" + }, + "peerDependencies": { + "@testing-library/react-native": ">=12", + "react": ">=18", + "react-native": ">=0.73", + "react-test-renderer": ">=18" + }, + "devDependencies": { + "@callstack/brownfield-navigation": "workspace:^", + "@callstack/react-native-brownfield": "workspace:^", + "@testing-library/react-native": "^13.3.3", + "@types/jest": "^30.0.0", + "@types/react": "^19.1.1", + "typescript": "5.9.3" + } +} diff --git a/apps/brownfield-example-shared-tests/src/global.d.ts b/apps/brownfield-example-shared-tests/src/global.d.ts new file mode 100644 index 00000000..4bd0eb9b --- /dev/null +++ b/apps/brownfield-example-shared-tests/src/global.d.ts @@ -0,0 +1,7 @@ +/* eslint-disable no-var */ +declare global { + var __brownieExampleCounter: number; + var __resetBrownieExampleStore: () => void; +} + +export {}; diff --git a/apps/brownfield-example-shared-tests/src/index.ts b/apps/brownfield-example-shared-tests/src/index.ts new file mode 100644 index 00000000..fc15bdc4 --- /dev/null +++ b/apps/brownfield-example-shared-tests/src/index.ts @@ -0,0 +1,4 @@ +export { runPostMessageTabSuite } from './suites/postMessageTab.suite'; +export { runCounterSuite } from './suites/counter.suite'; +export { runExpoRnAppSuite } from './suites/expoRnApp.suite'; +export { runHomeScreenSuite } from './suites/homeScreen.suite'; diff --git a/apps/brownfield-example-shared-tests/src/suites/counter.suite.tsx b/apps/brownfield-example-shared-tests/src/suites/counter.suite.tsx new file mode 100644 index 00000000..74492d3d --- /dev/null +++ b/apps/brownfield-example-shared-tests/src/suites/counter.suite.tsx @@ -0,0 +1,35 @@ +import type { ComponentType } from 'react'; +import { render, fireEvent, screen } from '@testing-library/react-native'; + +function resetBrownieExampleStore() { + const g = globalThis as typeof globalThis & { + __resetBrownieExampleStore?: () => void; + }; + g.__resetBrownieExampleStore?.(); +} + +/** + * Brownfield shared-store Counter examples (Brownie useStore). + */ +export function runCounterSuite

>( + appLabel: string, + Counter: ComponentType

, + props?: P +) { + describe(`Counter — ${appLabel}`, () => { + beforeEach(() => { + resetBrownieExampleStore(); + }); + + it('increments the displayed count when Increment is pressed', () => { + const counterProps = (props ?? ({} as P)) as P; + render(); + + expect(screen.getByText('Count: 0')).toBeTruthy(); + + fireEvent.press(screen.getByText('Increment')); + + expect(screen.getByText('Count: 1')).toBeTruthy(); + }); + }); +} diff --git a/apps/brownfield-example-shared-tests/src/suites/expoRnApp.suite.tsx b/apps/brownfield-example-shared-tests/src/suites/expoRnApp.suite.tsx new file mode 100644 index 00000000..a44745d0 --- /dev/null +++ b/apps/brownfield-example-shared-tests/src/suites/expoRnApp.suite.tsx @@ -0,0 +1,51 @@ +import type { ComponentType } from 'react'; +import { render, fireEvent, screen } from '@testing-library/react-native'; +import BrownfieldNavigation from '@callstack/brownfield-navigation'; + +type RNAppProps = { nativeOsVersionLabel?: string }; + +function resetBrownieExampleStore() { + const g = globalThis as typeof globalThis & { + __resetBrownieExampleStore?: () => void; + }; + g.__resetBrownieExampleStore?.(); +} + +const brownfieldNavigation = + BrownfieldNavigation as unknown as { + navigateToSettings: jest.Mock; + navigateToReferrals: jest.Mock; + }; + +/** + * Root RN surface used by Expo brownfield examples (navigation + counter). + */ +export function runExpoRnAppSuite( + appLabel: string, + RNApp: ComponentType +) { + describe(`Expo RNApp — ${appLabel}`, () => { + beforeEach(() => { + resetBrownieExampleStore(); + jest.clearAllMocks(); + }); + + it('calls native navigation when opening settings and referrals', () => { + render(); + + fireEvent.press(screen.getByText('Navigate to Settings')); + expect(brownfieldNavigation.navigateToSettings).toHaveBeenCalled(); + + fireEvent.press(screen.getByText('Navigate to Referrals')); + expect(brownfieldNavigation.navigateToReferrals).toHaveBeenCalledWith( + '123' + ); + }); + + it('renders the native OS version label when provided', () => { + render(); + + expect(screen.getByText('iOS 18')).toBeTruthy(); + }); + }); +} diff --git a/apps/brownfield-example-shared-tests/src/suites/homeScreen.suite.tsx b/apps/brownfield-example-shared-tests/src/suites/homeScreen.suite.tsx new file mode 100644 index 00000000..6390dae9 --- /dev/null +++ b/apps/brownfield-example-shared-tests/src/suites/homeScreen.suite.tsx @@ -0,0 +1,116 @@ +import type { ComponentType } from 'react'; +import { render, fireEvent, screen, act } from '@testing-library/react-native'; +import ReactNativeBrownfield from '@callstack/react-native-brownfield'; +import BrownfieldNavigation from '@callstack/brownfield-navigation'; + +type Nav = { + addListener: ( + event: string, + cb: () => void + ) => (() => void) | { remove?: () => void }; + canGoBack: () => boolean; + goBack: () => void; + push: (name: string, params?: object) => void; +}; + +function resetBrownieExampleStore() { + const g = globalThis as typeof globalThis & { + __resetBrownieExampleStore?: () => void; + }; + g.__resetBrownieExampleStore?.(); +} + +const brownfieldNavigation = + BrownfieldNavigation as unknown as { + navigateToSettings: jest.Mock; + navigateToReferrals: jest.Mock; + }; + +/** + * Home screen in the plain RN example: postMessage, back gesture, popToNative, brownfield navigation. + */ +export function runHomeScreenSuite

( + appLabel: string, + HomeScreen: ComponentType

+) { + describe(`HomeScreen — ${appLabel}`, () => { + const focusListeners: (() => void)[] = []; + + beforeEach(() => { + resetBrownieExampleStore(); + focusListeners.length = 0; + jest.clearAllMocks(); + }); + + function makeNavigation(overrides: Partial