refactor: export mocks per module (#329)

* refactor(android): parent handles own translation in stack

* refactor: export mocks per module instead of __mocks__ directory

- Add src/mock.ts for main TrueSheet exports
- Add src/navigation/mock.ts for navigation module
- Add src/reanimated/mock.ts for reanimated module
- Remove src/__mocks__ directory
- Update tsconfig.json and package.json to remove __mocks__ references
- Update tests to use new mock imports

* refactor: consolidate mocks into src/mocks folder

- Move mock files into src/mocks/ directory
- Export as /mock, /navigation/mock, /reanimated/mock
- Update package.json exports
- Update tests and documentation

* chore: simplify jest.setup.js mocks

* test: add navigation mock tests

* ci: extract platform builds to separate workflow
This commit is contained in:
Jovanni Lo 2025-12-16 15:28:40 +08:00 committed by GitHub
parent d6f59459e7
commit 98694a3228
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 619 additions and 247 deletions

118
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,118 @@
name: Build
on:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-android:
runs-on: ubuntu-latest
env:
TURBO_CACHE_DIR: .turbo/android
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup
uses: ./.github/actions/setup
- name: Cache turborepo for Android
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ${{ env.TURBO_CACHE_DIR }}
key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-turborepo-android-
- name: Check turborepo cache for Android
run: |
TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status")
if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi
- name: Install JDK
if: env.turbo_cache_hit != 1
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: 'zulu'
java-version: '17'
- name: Finalize Android SDK
if: env.turbo_cache_hit != 1
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"
- name: Cache Gradle
if: env.turbo_cache_hit != 1
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.gradle/wrapper
~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('example/bare/android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build example for Android
env:
JAVA_OPTS: "-XX:MaxHeapSize=6g"
run: |
yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}"
build-ios:
runs-on: macos-latest
env:
XCODE_VERSION: 16.3
TURBO_CACHE_DIR: .turbo/ios
RCT_USE_RN_DEP: 1
RCT_USE_PREBUILT_RNCORE: 1
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup
uses: ./.github/actions/setup
- name: Cache turborepo for iOS
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ${{ env.TURBO_CACHE_DIR }}
key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-turborepo-ios-
- name: Check turborepo cache for iOS
run: |
TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status")
if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi
- name: Use appropriate Xcode version
if: env.turbo_cache_hit != 1
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
with:
xcode-version: ${{ env.XCODE_VERSION }}
- name: Install cocoapods
if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true'
run: |
cd example/bare
bundle install
bundle exec pod repo update --verbose
bundle exec pod install --project-directory=ios
- name: Build example for iOS
run: |
yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}"

View File

@ -56,111 +56,3 @@ jobs:
- name: Build package
run: yarn prepare
build-android:
runs-on: ubuntu-latest
env:
TURBO_CACHE_DIR: .turbo/android
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup
uses: ./.github/actions/setup
- name: Cache turborepo for Android
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ${{ env.TURBO_CACHE_DIR }}
key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-turborepo-android-
- name: Check turborepo cache for Android
run: |
TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status")
if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi
- name: Install JDK
if: env.turbo_cache_hit != 1
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: 'zulu'
java-version: '17'
- name: Finalize Android SDK
if: env.turbo_cache_hit != 1
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"
- name: Cache Gradle
if: env.turbo_cache_hit != 1
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.gradle/wrapper
~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('example/bare/android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build example for Android
env:
JAVA_OPTS: "-XX:MaxHeapSize=6g"
run: |
yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}"
build-ios:
runs-on: macos-latest
env:
XCODE_VERSION: 16.3
TURBO_CACHE_DIR: .turbo/ios
RCT_USE_RN_DEP: 1
RCT_USE_PREBUILT_RNCORE: 1
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup
uses: ./.github/actions/setup
- name: Cache turborepo for iOS
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ${{ env.TURBO_CACHE_DIR }}
key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-turborepo-ios-
- name: Check turborepo cache for iOS
run: |
TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status")
if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi
- name: Use appropriate Xcode version
if: env.turbo_cache_hit != 1
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
with:
xcode-version: ${{ env.XCODE_VERSION }}
- name: Install cocoapods
if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true'
run: |
cd example/bare
bundle install
bundle exec pod repo update --verbose
bundle exec pod install --project-directory=ios
- name: Build example for iOS
run: |
yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}"

View File

@ -1,5 +1,4 @@
lib
build
src/__mocks__
docs/build
docs/.docusaurus

View File

@ -91,10 +91,23 @@ export const App = () => {
## Testing
TrueSheet includes built-in Jest mocks for easy testing. Simply mock the package in your tests:
TrueSheet exports mocks for easy testing:
```tsx
jest.mock('@lodev09/react-native-true-sheet');
// Main component
jest.mock('@lodev09/react-native-true-sheet', () =>
require('@lodev09/react-native-true-sheet/mock')
);
// Navigation (if using)
jest.mock('@lodev09/react-native-true-sheet/navigation', () =>
require('@lodev09/react-native-true-sheet/navigation/mock')
);
// Reanimated (if using)
jest.mock('@lodev09/react-native-true-sheet/reanimated', () =>
require('@lodev09/react-native-true-sheet/reanimated/mock')
);
```
All methods (`present`, `dismiss`, `resize`) are mocked as Jest functions, allowing you to test your components without native dependencies.

View File

@ -387,6 +387,36 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
viewController.positionFooter()
}
// ==================== Stack Translation ====================
/**
* Updates this sheet's translation based on its direct child's position.
* Propagates additional translation to parent so it stays behind this sheet.
*/
fun updateTranslationForChild(childSheetTop: Int) {
if (!viewController.isDialogVisible || viewController.isExpanded) return
val mySheetTop = viewController.getExpectedSheetTop(viewController.currentDetentIndex)
val newTranslation = maxOf(0, childSheetTop - mySheetTop)
val additionalTranslation = newTranslation - viewController.currentTranslationY
viewController.translateDialog(newTranslation)
if (additionalTranslation > 0) {
TrueSheetDialogObserver.getParentSheet(this)?.addTranslation(additionalTranslation)
}
}
/**
* Adds translation to this sheet and propagates to parent.
*/
private fun addTranslation(amount: Int) {
if (!viewController.isDialogVisible || viewController.isExpanded) return
viewController.translateDialog(viewController.currentTranslationY + amount)
TrueSheetDialogObserver.getParentSheet(this)?.addTranslation(amount)
}
companion object {
const val TAG_NAME = "TrueSheet"
}

View File

@ -475,6 +475,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
val currentSheetTop: Int
get() = bottomSheetView?.top ?: screenHeight
val currentTranslationY: Int
get() = bottomSheetView?.translationY?.toInt() ?: 0
fun getExpectedSheetTop(detentIndex: Int): Int {
if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
return realScreenHeight - getDetentHeight(detents[detentIndex])

View File

@ -21,7 +21,7 @@ object TrueSheetDialogObserver {
?.takeIf { it.viewController.isPresented && it.viewController.isDialogVisible }
val childSheetTop = sheetView.viewController.getExpectedSheetTop(detentIndex)
updateParentTranslations(childSheetTop)
parentSheet?.updateTranslationForChild(childSheetTop)
if (!presentedSheetStack.contains(sheetView)) {
presentedSheetStack.add(sheetView)
@ -55,13 +55,15 @@ object TrueSheetDialogObserver {
val index = presentedSheetStack.indexOf(sheetView)
if (index <= 0) return
val parentSheet = presentedSheetStack[index - 1]
// Post to ensure layout is complete before reading position
sheetView.viewController.post {
val childMinSheetTop = sheetView.viewController.getExpectedSheetTop(0)
val childCurrentSheetTop = sheetView.viewController.getExpectedSheetTop(sheetView.viewController.currentDetentIndex)
// Cap to minimum detent position
val childSheetTop = maxOf(childMinSheetTop, childCurrentSheetTop)
updateParentTranslations(childSheetTop, untilIndex = index)
parentSheet.updateTranslationForChild(childSheetTop)
}
}
}
@ -94,23 +96,14 @@ object TrueSheetDialogObserver {
}
/**
* Translates parent sheets down to match the child sheet's position.
* @param childSheetTop The top position of the child sheet
* @param untilIndex If specified, only update sheets up to this index (exclusive)
* Gets the parent sheet of the given sheet, if any.
*/
private fun updateParentTranslations(childSheetTop: Int, untilIndex: Int = presentedSheetStack.size) {
for (i in 0 until untilIndex) {
val parentSheet = presentedSheetStack[i]
if (!parentSheet.viewController.isDialogVisible) continue
if (parentSheet.viewController.isExpanded) continue
val parentSheetTop = parentSheet.viewController.getExpectedSheetTop(parentSheet.viewController.currentDetentIndex)
if (parentSheetTop < childSheetTop) {
val translationY = childSheetTop - parentSheetTop
parentSheet.viewController.translateDialog(translationY)
} else {
parentSheet.viewController.translateDialog(0)
}
@JvmStatic
fun getParentSheet(sheetView: TrueSheetView): TrueSheetView? {
synchronized(presentedSheetStack) {
val index = presentedSheetStack.indexOf(sheetView)
if (index <= 0) return null
return presentedSheetStack[index - 1]
}
}
}

View File

@ -8,11 +8,25 @@ Testing components that use `TrueSheet` is straightforward with the built-in Jes
## Setup
Add the mock to your Jest setup file.
Add the mocks to your Jest setup file:
```js
// jest.setup.js
jest.mock('@lodev09/react-native-true-sheet');
// Main component
jest.mock('@lodev09/react-native-true-sheet', () =>
require('@lodev09/react-native-true-sheet/mock')
);
// Navigation (if using)
jest.mock('@lodev09/react-native-true-sheet/navigation', () =>
require('@lodev09/react-native-true-sheet/navigation/mock')
);
// Reanimated (if using)
jest.mock('@lodev09/react-native-true-sheet/reanimated', () =>
require('@lodev09/react-native-true-sheet/reanimated/mock')
);
```
Configure Jest to use the setup file in your `package.json`:
@ -25,6 +39,27 @@ Configure Jest to use the setup file in your `package.json`:
}
```
## Available Mocks
### Main Module (`/mock`)
- `TrueSheet` - Component with mocked `present`, `dismiss`, `resize` methods
- `TrueSheetProvider` - Pass-through provider
- `useTrueSheet` - Hook returning mocked methods
### Navigation Module (`/navigation/mock`)
- `createTrueSheetNavigator` - Mocked navigator factory
- `TrueSheetActions` - Mocked action creators
- `useTrueSheetNavigation` - Hook returning mocked navigation object
### Reanimated Module (`/reanimated/mock`)
- `ReanimatedTrueSheet` - Component with mocked methods
- `ReanimatedTrueSheetProvider` - Pass-through provider
- `useReanimatedTrueSheet` - Hook returning mocked shared values
- `useReanimatedPositionChangeHandler` - Mocked handler hook
## Testing Static Methods
All static methods are mocked as Jest functions.
@ -34,13 +69,13 @@ import { TrueSheet } from '@lodev09/react-native-true-sheet';
it('should present sheet', async () => {
await TrueSheet.present('my-sheet', 0);
expect(TrueSheet.present).toHaveBeenCalledWith('my-sheet', 0);
});
it('should dismiss sheet', async () => {
await TrueSheet.dismiss('my-sheet');
expect(TrueSheet.dismiss).toHaveBeenCalledWith('my-sheet');
});
```
@ -56,11 +91,29 @@ it('should render sheet content', () => {
<Text>Sheet Content</Text>
</TrueSheet>
);
expect(getByText('Sheet Content')).toBeDefined();
});
```
## Testing Reanimated Integration
```tsx
import {
useReanimatedTrueSheet,
ReanimatedTrueSheetProvider,
} from '@lodev09/react-native-true-sheet/reanimated';
it('should return mocked shared values', () => {
const { result } = renderHook(() => useReanimatedTrueSheet(), {
wrapper: ReanimatedTrueSheetProvider,
});
expect(result.current.animatedPosition.value).toBe(0);
expect(result.current.animatedIndex.value).toBe(-1);
});
```
## Best Practices
Clear mock calls between tests to avoid interference.
@ -77,4 +130,4 @@ describe('MyComponent', () => {
:::tip
All methods return resolved Promises, so remember to `await` them in your tests.
:::
:::

View File

@ -1,11 +1,11 @@
/* eslint-env jest */
// Mock the native module
jest.mock('./src/specs/NativeTrueSheetModule', () => ({
__esModule: true,
default: {
present: jest.fn(),
dismiss: jest.fn(),
dismissAll: jest.fn(),
presentByRef: jest.fn(),
dismissByRef: jest.fn(),
},
}));
@ -13,7 +13,7 @@ jest.mock('./src/specs/NativeTrueSheetModule', () => ({
jest.mock('./src/fabric/TrueSheetViewNativeComponent', () => {
const React = require('react');
const { View } = require('react-native');
return {
__esModule: true,
default: React.forwardRef((props, ref) => {
@ -25,7 +25,7 @@ jest.mock('./src/fabric/TrueSheetViewNativeComponent', () => {
jest.mock('./src/fabric/TrueSheetContainerViewNativeComponent', () => {
const React = require('react');
const { View } = require('react-native');
return {
__esModule: true,
default: React.forwardRef((props, ref) => {
@ -37,7 +37,7 @@ jest.mock('./src/fabric/TrueSheetContainerViewNativeComponent', () => {
jest.mock('./src/fabric/TrueSheetContentViewNativeComponent', () => {
const React = require('react');
const { ScrollView } = require('react-native');
return {
__esModule: true,
default: React.forwardRef((props, ref) => {
@ -46,10 +46,10 @@ jest.mock('./src/fabric/TrueSheetContentViewNativeComponent', () => {
};
});
jest.mock('./src/fabric/TrueSheetFooterViewNativeComponent', () => {
jest.mock('./src/fabric/TrueSheetHeaderViewNativeComponent', () => {
const React = require('react');
const { View } = require('react-native');
return {
__esModule: true,
default: React.forwardRef((props, ref) => {
@ -58,50 +58,48 @@ jest.mock('./src/fabric/TrueSheetFooterViewNativeComponent', () => {
};
});
// Mock reanimated if it's being used
jest.mock('react-native-reanimated', () => {
jest.mock('./src/fabric/TrueSheetFooterViewNativeComponent', () => {
const React = require('react');
const { View } = require('react-native');
return {
default: {
call: () => {},
createAnimatedComponent: (component) => component,
},
useSharedValue: jest.fn(() => ({ value: 0 })),
useAnimatedStyle: jest.fn((callback) => callback()),
withTiming: jest.fn((value) => value),
withSpring: jest.fn((value) => value),
runOnJS: jest.fn((fn) => fn),
createAnimatedComponent: (component) => component,
Easing: {
bezier: jest.fn(() => jest.fn()),
linear: jest.fn(),
ease: jest.fn(),
quad: jest.fn(),
cubic: jest.fn(),
sin: jest.fn(),
circle: jest.fn(),
exp: jest.fn(),
elastic: jest.fn(),
back: jest.fn(),
bounce: jest.fn(),
in: jest.fn(),
out: jest.fn(),
inOut: jest.fn(),
},
__esModule: true,
default: React.forwardRef((props, ref) => {
return React.createElement(View, { ...props, ref });
}),
};
});
// Mock worklets if it's being used
jest.mock('react-native-worklets-core', () => ({}), { virtual: true });
// Mock react-native-reanimated
jest.mock('react-native-reanimated', () => ({
default: {
call: () => {},
createAnimatedComponent: (component) => component,
},
useSharedValue: jest.fn((initial) => ({ value: initial })),
useAnimatedStyle: jest.fn((callback) => callback()),
withTiming: jest.fn((value) => value),
withSpring: jest.fn((value) => value),
runOnJS: jest.fn((fn) => fn),
createAnimatedComponent: (component) => component,
useEvent: jest.fn(() => jest.fn()),
useHandler: jest.fn(() => ({ context: {}, doDependenciesDiffer: false })),
Easing: {
bezier: jest.fn(() => jest.fn()),
},
}));
// Mock react-native-worklets
jest.mock('react-native-worklets-core', () => ({}), { virtual: true });
jest.mock(
'react-native-worklets',
() => ({
scheduleOnRN: jest.fn((fn) => fn),
scheduleOnJS: jest.fn((fn) => fn),
useSharedValue: jest.fn(() => ({ value: 0 })),
useSharedValue: jest.fn((initial) => ({ value: initial })),
useWorklet: jest.fn((fn) => fn),
runOnJS: jest.fn((fn) => fn),
runOnUI: jest.fn((fn) => fn),
}),
{ virtual: true }
);
);

View File

@ -11,6 +11,9 @@
"types": "./lib/typescript/src/index.d.ts",
"default": "./lib/module/index.js"
},
"./mock": "./lib/module/mocks/index.js",
"./navigation/mock": "./lib/module/mocks/navigation.js",
"./reanimated/mock": "./lib/module/mocks/reanimated.js",
"./reanimated": {
"source": "./src/reanimated/index.ts",
"types": "./lib/typescript/src/reanimated/index.d.ts",
@ -164,7 +167,6 @@
],
"testPathIgnorePatterns": [
"/node_modules/",
"<rootDir>/src/__mocks__/",
"<rootDir>/example/"
]
},
@ -214,7 +216,6 @@
"react-native-builder-bob": {
"source": "src",
"output": "lib",
"exclude": "**/{__tests__,__fixtures__}/**",
"targets": [
[
"module",

View File

@ -1,63 +0,0 @@
import React from 'react';
import { View } from 'react-native';
// Mock TrueSheet class component
export class TrueSheet extends React.Component {
static instances = {};
// Static methods
static dismiss = jest.fn((name, animated = true) => Promise.resolve());
static present = jest.fn((name, index = 0, animated = true) => Promise.resolve());
static resize = jest.fn((name, index) => Promise.resolve());
// Instance methods
dismiss = jest.fn((animated = true) => Promise.resolve());
present = jest.fn((index = 0, animated = true) => Promise.resolve());
resize = jest.fn((index) => Promise.resolve());
componentDidMount() {
const { name } = this.props;
if (name) {
TrueSheet.instances[name] = this;
}
}
componentWillUnmount() {
const { name } = this.props;
if (name) {
delete TrueSheet.instances[name];
}
}
render() {
const { children, header, footer, style, ...rest } = this.props;
return (
<View style={style} {...rest}>
{header}
{children}
{footer}
</View>
);
}
}
// Mock ReanimatedTrueSheet
export class ReanimatedTrueSheet extends TrueSheet {
render() {
return <TrueSheet {...this.props} />;
}
}
// Mock ReanimatedTrueSheetProvider
export const ReanimatedTrueSheetProvider = ({ children }) => <>{children}</>;
// Mock hooks
export const useReanimatedTrueSheet = jest.fn(() => ({
animatedPosition: { value: 0 },
animatedIndex: { value: -1 },
}));
export const useReanimatedPositionChangeHandler = jest.fn((callback) => jest.fn());
// Re-export types (these will be no-ops in JS but useful for TS consumers)
export * from '../TrueSheet.types';

View File

@ -1,15 +1,19 @@
import { Text } from 'react-native';
import { render } from '@testing-library/react-native';
// Manually import the mock to test it
const {
TrueSheet,
// Import the mocks
import { TrueSheet, TrueSheetProvider, useTrueSheet } from '../mocks';
import {
createTrueSheetNavigator,
TrueSheetActions,
useTrueSheetNavigation,
} from '../mocks/navigation';
import {
ReanimatedTrueSheet,
ReanimatedTrueSheetProvider,
useReanimatedTrueSheet,
useReanimatedPositionChangeHandler,
} = require('../__mocks__');
} from '../mocks/reanimated';
describe('TrueSheet Mocks', () => {
beforeEach(() => {
@ -74,8 +78,83 @@ describe('TrueSheet Mocks', () => {
});
});
describe('TrueSheetProvider Mock', () => {
it('should render children without modification', () => {
const { getByText } = render(
<TrueSheetProvider>
<Text>Provider Content</Text>
</TrueSheetProvider>
);
expect(getByText('Provider Content')).toBeDefined();
});
});
describe('useTrueSheet Hook Mock', () => {
it('should return mock methods', () => {
const result = useTrueSheet();
expect(result.present).toBeDefined();
expect(result.dismiss).toBeDefined();
expect(result.resize).toBeDefined();
});
});
describe('createTrueSheetNavigator Mock', () => {
it('should return navigator components', () => {
const Navigator = createTrueSheetNavigator();
expect(Navigator.Navigator).toBeDefined();
expect(Navigator.Screen).toBeDefined();
expect(Navigator.Group).toBeDefined();
});
it('should be a jest mock function', () => {
jest.clearAllMocks();
createTrueSheetNavigator();
expect(createTrueSheetNavigator).toHaveBeenCalledTimes(1);
});
});
describe('TrueSheetActions Mock', () => {
it('should have all action creators', () => {
expect(TrueSheetActions.push).toBeDefined();
expect(TrueSheetActions.pop).toBeDefined();
expect(TrueSheetActions.popTo).toBeDefined();
expect(TrueSheetActions.popToTop).toBeDefined();
expect(TrueSheetActions.replace).toBeDefined();
expect(TrueSheetActions.resize).toBeDefined();
expect(TrueSheetActions.dismiss).toBeDefined();
expect(TrueSheetActions.remove).toBeDefined();
});
it('should return action objects', () => {
expect(TrueSheetActions.push('TestScreen')).toEqual({
type: 'PUSH',
payload: { name: 'TestScreen', params: undefined },
});
expect(TrueSheetActions.resize(1)).toEqual({ type: 'RESIZE', index: 1 });
expect(TrueSheetActions.dismiss()).toEqual({ type: 'DISMISS' });
});
});
describe('useTrueSheetNavigation Hook Mock', () => {
it('should return navigation object with all methods', () => {
const navigation = useTrueSheetNavigation();
expect(navigation.navigate).toBeDefined();
expect(navigation.goBack).toBeDefined();
expect(navigation.push).toBeDefined();
expect(navigation.pop).toBeDefined();
expect(navigation.resize).toBeDefined();
expect(navigation.dispatch).toBeDefined();
});
it('should be a jest mock function', () => {
jest.clearAllMocks();
useTrueSheetNavigation();
expect(useTrueSheetNavigation).toHaveBeenCalledTimes(1);
});
});
describe('ReanimatedTrueSheet Component Mock', () => {
it('should render as TrueSheet', () => {
it('should render children', () => {
const { getByText } = render(
<ReanimatedTrueSheet name="test" initialDetentIndex={0}>
<Text>Reanimated Content</Text>
@ -101,8 +180,10 @@ describe('TrueSheet Mocks', () => {
const result = useReanimatedTrueSheet();
expect(result.animatedPosition).toBeDefined();
expect(result.animatedIndex).toBeDefined();
expect(result.animatedDetent).toBeDefined();
expect(result.animatedPosition.value).toBe(0);
expect(result.animatedIndex.value).toBe(-1);
expect(result.animatedDetent.value).toBe(0);
});
it('should be a jest mock function', () => {

80
src/mocks/index.ts Normal file
View File

@ -0,0 +1,80 @@
import React, { createElement, isValidElement, type ReactNode } from 'react';
import { View } from 'react-native';
import type { TrueSheetProps, TrueSheetRef, TrueSheetContextMethods } from '../TrueSheet.types';
interface TrueSheetState {
shouldRenderNativeView: boolean;
}
/**
* Mock TrueSheet component for testing.
* Import from '@lodev09/react-native-true-sheet/mock' in your test setup.
*/
export class TrueSheet
extends React.Component<TrueSheetProps, TrueSheetState>
implements TrueSheetRef
{
static instances: Record<string, TrueSheet> = {};
static dismiss = jest.fn((_name: string, _animated?: boolean) => Promise.resolve());
static present = jest.fn((_name: string, _index?: number, _animated?: boolean) =>
Promise.resolve()
);
static resize = jest.fn((_name: string, _index: number) => Promise.resolve());
dismiss = jest.fn((_animated?: boolean) => Promise.resolve());
present = jest.fn((_index?: number, _animated?: boolean) => Promise.resolve());
resize = jest.fn((_index: number) => Promise.resolve());
componentDidMount() {
const { name } = this.props;
if (name) {
TrueSheet.instances[name] = this;
}
}
componentWillUnmount() {
const { name } = this.props;
if (name) {
delete TrueSheet.instances[name];
}
}
private renderHeader(): ReactNode {
const { header } = this.props;
if (!header) return null;
return isValidElement(header) ? header : createElement(header);
}
private renderFooter(): ReactNode {
const { footer } = this.props;
if (!footer) return null;
return isValidElement(footer) ? footer : createElement(footer);
}
render() {
const { children, style } = this.props;
return React.createElement(View, { style }, this.renderHeader(), children, this.renderFooter());
}
}
/**
* Mock TrueSheetProvider for testing.
*/
export function TrueSheetProvider({ children }: { children: React.ReactNode }) {
return children;
}
/**
* Mock useTrueSheet hook for testing.
*/
export function useTrueSheet(): TrueSheetContextMethods {
return {
present: TrueSheet.present,
dismiss: TrueSheet.dismiss,
resize: TrueSheet.resize,
};
}
export * from '../TrueSheet.types';

72
src/mocks/navigation.ts Normal file
View File

@ -0,0 +1,72 @@
import type { ParamListBase } from '@react-navigation/native';
import type { TrueSheetNavigationProp } from '../navigation/types';
/**
* Mock createTrueSheetNavigator for testing.
* Import from '@lodev09/react-native-true-sheet/navigation/mock' in your test setup.
*/
export const createTrueSheetNavigator = jest.fn(() => ({
Navigator: jest.fn(({ children }: { children: React.ReactNode }) => children),
Screen: jest.fn(() => null),
Group: jest.fn(() => null),
}));
/**
* Mock TrueSheetActions for testing.
*/
export const TrueSheetActions = {
push: jest.fn((name: string, params?: object) => ({ type: 'PUSH', payload: { name, params } })),
pop: jest.fn((count?: number) => ({ type: 'POP', payload: { count } })),
popTo: jest.fn((name: string, params?: object) => ({
type: 'POP_TO',
payload: { name, params },
})),
popToTop: jest.fn(() => ({ type: 'POP_TO_TOP' })),
replace: jest.fn((name: string, params?: object) => ({
type: 'REPLACE',
payload: { name, params },
})),
resize: jest.fn((index: number) => ({ type: 'RESIZE', index })),
dismiss: jest.fn(() => ({ type: 'DISMISS' })),
remove: jest.fn(() => ({ type: 'REMOVE' })),
};
/**
* Mock useTrueSheetNavigation hook for testing.
*/
export const useTrueSheetNavigation = jest.fn(
<T extends ParamListBase = ParamListBase>() =>
({
navigate: jest.fn(),
goBack: jest.fn(),
reset: jest.fn(),
setParams: jest.fn(),
dispatch: jest.fn(),
isFocused: jest.fn(() => true),
canGoBack: jest.fn(() => true),
getId: jest.fn(),
getParent: jest.fn(),
getState: jest.fn(),
addListener: jest.fn(() => jest.fn()),
removeListener: jest.fn(),
setOptions: jest.fn(),
push: jest.fn(),
pop: jest.fn(),
popTo: jest.fn(),
popToTop: jest.fn(),
replace: jest.fn(),
resize: jest.fn(),
}) as unknown as TrueSheetNavigationProp<T>
);
export type { TrueSheetActionType } from '../navigation/TrueSheetRouter';
export type { DetentInfoEventPayload, PositionChangeEventPayload } from '../TrueSheet.types';
export type {
TrueSheetNavigationEventMap,
TrueSheetNavigationHelpers,
TrueSheetNavigationOptions,
TrueSheetNavigationProp,
TrueSheetNavigationState,
TrueSheetScreenProps,
} from '../navigation/types';

102
src/mocks/reanimated.ts Normal file
View File

@ -0,0 +1,102 @@
import React, { createElement, isValidElement, type ReactNode } from 'react';
import { View } from 'react-native';
import type { SharedValue } from 'react-native-reanimated';
import type { TrueSheetProps, TrueSheetRef, PositionChangeEventPayload } from '../TrueSheet.types';
interface TrueSheetState {
shouldRenderNativeView: boolean;
}
interface MockReanimatedTrueSheetContextValue {
animatedPosition: SharedValue<number>;
animatedIndex: SharedValue<number>;
animatedDetent: SharedValue<number>;
}
const createMockSharedValue = <T>(initialValue: T): SharedValue<T> =>
({
value: initialValue,
get: () => initialValue,
set: () => {},
addListener: () => () => {},
removeListener: () => {},
modify: () => {},
}) as unknown as SharedValue<T>;
/**
* Mock ReanimatedTrueSheet component for testing.
* Import from '@lodev09/react-native-true-sheet/reanimated/mock' in your test setup.
*/
export class ReanimatedTrueSheet
extends React.Component<TrueSheetProps, TrueSheetState>
implements TrueSheetRef
{
static instances: Record<string, ReanimatedTrueSheet> = {};
static dismiss = jest.fn((_name: string, _animated?: boolean) => Promise.resolve());
static present = jest.fn((_name: string, _index?: number, _animated?: boolean) =>
Promise.resolve()
);
static resize = jest.fn((_name: string, _index: number) => Promise.resolve());
dismiss = jest.fn((_animated?: boolean) => Promise.resolve());
present = jest.fn((_index?: number, _animated?: boolean) => Promise.resolve());
resize = jest.fn((_index: number) => Promise.resolve());
componentDidMount() {
const { name } = this.props;
if (name) {
ReanimatedTrueSheet.instances[name] = this;
}
}
componentWillUnmount() {
const { name } = this.props;
if (name) {
delete ReanimatedTrueSheet.instances[name];
}
}
private renderHeader(): ReactNode {
const { header } = this.props;
if (!header) return null;
return isValidElement(header) ? header : createElement(header);
}
private renderFooter(): ReactNode {
const { footer } = this.props;
if (!footer) return null;
return isValidElement(footer) ? footer : createElement(footer);
}
render() {
const { children, style } = this.props;
return React.createElement(View, { style }, this.renderHeader(), children, this.renderFooter());
}
}
/**
* Mock ReanimatedTrueSheetProvider for testing.
*/
export function ReanimatedTrueSheetProvider({ children }: { children: React.ReactNode }) {
return children;
}
/**
* Mock useReanimatedTrueSheet hook for testing.
*/
export const useReanimatedTrueSheet = jest.fn(
(): MockReanimatedTrueSheetContextValue => ({
animatedPosition: createMockSharedValue(0),
animatedIndex: createMockSharedValue(-1),
animatedDetent: createMockSharedValue(0),
})
);
/**
* Mock useReanimatedPositionChangeHandler hook for testing.
*/
export const useReanimatedPositionChangeHandler = jest.fn(
(_handler: (payload: PositionChangeEventPayload) => void, _dependencies?: unknown[]) => jest.fn()
);

View File

@ -28,5 +28,5 @@
"target": "ESNext",
"verbatimModuleSyntax": true
},
"exclude": ["lib", "build", "src/__mocks__", "docs"]
"exclude": ["lib", "build", "docs"]
}