Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 7 additions & 0 deletions .changeset/orange-dancers-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@callstack/brownfield-example-shared-tests': minor
'@callstack/brownie': minor
'@callstack/brownfield-cli': minor
---

Jest unit tests
27 changes: 25 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
12 changes: 12 additions & 0 deletions apps/ExpoApp54/__tests__/brownfield.example.test.tsx
Original file line number Diff line number Diff line change
@@ -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);
30 changes: 30 additions & 0 deletions apps/ExpoApp54/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const path = require('path');

module.exports = {
preset: 'jest-expo',
setupFilesAfterEnv: ['<rootDir>/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'
),
'^@/(.*)$': '<rootDir>/$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)/)',
],
};
1 change: 1 addition & 0 deletions apps/ExpoApp54/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('@callstack/brownfield-example-shared-tests/jest/setup');
9 changes: 8 additions & 1 deletion apps/ExpoApp54/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
12 changes: 12 additions & 0 deletions apps/ExpoApp55/__tests__/brownfield.example.test.tsx
Original file line number Diff line number Diff line change
@@ -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);
8 changes: 8 additions & 0 deletions apps/ExpoApp55/eslint.config.js
Original file line number Diff line number Diff line change
@@ -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,
},
},
]);
33 changes: 33 additions & 0 deletions apps/ExpoApp55/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const path = require('path');

module.exports = {
preset: 'jest-expo',
setupFilesAfterEnv: ['<rootDir>/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$': '<rootDir>/jest/cssMock.js',
'^@/assets/(.*)$': '<rootDir>/assets/$1',
'^@/(.*)$': '<rootDir>/src/$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)/)',
],
};
1 change: 1 addition & 0 deletions apps/ExpoApp55/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('@callstack/brownfield-example-shared-tests/jest/setup');
2 changes: 2 additions & 0 deletions apps/ExpoApp55/jest/cssMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** Stub CSS imports for Jest (used by Expo Router / bundler pipeline). */
module.exports = {};
10 changes: 9 additions & 1 deletion apps/ExpoApp55/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -46,9 +47,16 @@
"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",
"globals": "^17.6.0",
"jest": "^29.7.0",
"jest-expo": "~55.0.0",
"react-test-renderer": "19.2.0",
"typescript": "~5.9.2"
Comment thread
alpharius-ck marked this conversation as resolved.
},
"private": true,
Expand Down
3 changes: 1 addition & 2 deletions apps/RNApp/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
12 changes: 12 additions & 0 deletions apps/RNApp/__tests__/brownfield.example.test.tsx
Original file line number Diff line number Diff line change
@@ -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 });
25 changes: 25 additions & 0 deletions apps/RNApp/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
const path = require('node:path');

module.exports = {
preset: '@react-native/jest-preset',
setupFilesAfterEnv: ['<rootDir>/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)/)',
],
};
1 change: 1 addition & 0 deletions apps/RNApp/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('@callstack/brownfield-example-shared-tests/jest/setup');
4 changes: 3 additions & 1 deletion apps/RNApp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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",
Expand All @@ -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",
Expand Down
56 changes: 56 additions & 0 deletions apps/brownfield-example-shared-tests/jest/setup.js
Original file line number Diff line number Diff line change
@@ -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,
];
},
}));
28 changes: 28 additions & 0 deletions apps/brownfield-example-shared-tests/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
7 changes: 7 additions & 0 deletions apps/brownfield-example-shared-tests/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* eslint-disable no-var */
declare global {
var __brownieExampleCounter: number;
var __resetBrownieExampleStore: () => void;
}

export {};
4 changes: 4 additions & 0 deletions apps/brownfield-example-shared-tests/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading
Loading