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:
parent
d6f59459e7
commit
98694a3228
118
.github/workflows/build.yml
vendored
Normal file
118
.github/workflows/build.yml
vendored
Normal 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 }}"
|
||||
108
.github/workflows/ci.yml
vendored
108
.github/workflows/ci.yml
vendored
@ -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 }}"
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
lib
|
||||
build
|
||||
src/__mocks__
|
||||
docs/build
|
||||
docs/.docusaurus
|
||||
|
||||
17
README.md
17
README.md
@ -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.
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
:::
|
||||
:::
|
||||
|
||||
@ -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 }
|
||||
);
|
||||
);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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';
|
||||
@ -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
80
src/mocks/index.ts
Normal 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
72
src/mocks/navigation.ts
Normal 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
102
src/mocks/reanimated.ts
Normal 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()
|
||||
);
|
||||
@ -28,5 +28,5 @@
|
||||
"target": "ESNext",
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"exclude": ["lib", "build", "src/__mocks__", "docs"]
|
||||
"exclude": ["lib", "build", "docs"]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user