Skip to content

Commit

Permalink
feat(support): Vitest plugin (#29)
Browse files Browse the repository at this point in the history
* feat(support): Vitest plugin

* Document usage with Vitest

* Fix local tests env and plugin

* Fix native mocks on Vitest

* Add build to CI workflow
  • Loading branch information
JoseLion authored Jul 6, 2024
1 parent a1b6162 commit 45b6821
Show file tree
Hide file tree
Showing 21 changed files with 247 additions and 113 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
node-version: ${{ matrix.nodejs }}
cache: yarn
- run: yarn install --immutable
- run: yarn build
- run: yarn compile
- run: yarn lint
- run: yarn test
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,28 @@ import "react-native-testing-mocks/register";
// ...rest of your setup code
```

### Mocha.js Example
### With Vitest

Some frameworks also provide mechanisms to load setup modules. In Mocha.js, you can use the `--require` CLI option:
Ideally, Vitest should be able to replace Babel when it comes to resolving and transforming the React Native code. However, so much more goes on in [@react-native/babel-preset](https://www.npmjs.com/package/@react-native/babel-preset) that replacing this module with Vite is not only a hard task but also increases the risk of something going wrong. So, until React Native delivers their code in a more conventional format (CommonJS/ESM), is my opinion that it's safer to keep using Babel to transform React Native's code with Vitest.

That being said, this package also provides a Vite Plugin you can add to your `vitest.config.ts` configuration file:

```ts
import { reactNativePlugin } from "react-native-testing-mocks/vitest";
import { defineConfig } from "vitest/config";

export default defineConfig({
plugins: [reactNativePlugin()],
test: {
include: ["test/**/*.test.ts?(x)"],
setupFiles: "./test/setup.ts",
},
});
```

### With Mocha

Some frameworks like Mocha provide a mechanism to require modules during the Node.js execution. For instance, you can use the `--require` CLI option:

```bash
mocha --require react-native-testing-mocks/register
Expand All @@ -91,9 +110,9 @@ Or you can add it to the `.mocharc.json` file:
"require": [
"ts-node/register",
"react-native-testing-mocks/register",
"test/hooks.ts"
"./test/setup.ts"
],
"spec": ["test/**/*.test.*"]
"spec": ["test/**/*.test.ts?(x)"]
}
```

Expand Down
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,25 @@
"types": "./dist/register.d.ts",
"default": "./dist/register.js"
},
"./vitest": {
"import": "./dist/vitest.js",
"require": "./dist/vitest.cjs",
"types": "./dist/vitest/plugin.d.ts",
"default": "./dist/vitest.js"
},
"./vitest/env": {
"import": "./dist/vitestEnv.js",
"require": "./dist/vitestEnv.cjs",
"types": "./dist/vitest/env.d.ts",
"default": "./dist/vitestEnv.js"
},
"./package.json": "./package.json"
},
"files": [
"./dist",
"./src/",
"./register.d.ts",
"./vitest.d.ts",
"./package.json"
],
"engines": {
Expand Down
1 change: 1 addition & 0 deletions register.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./dist/register";
3 changes: 3 additions & 0 deletions src/helpers/commons.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createRequire } from "module";
import path from "path";

type ExportsLike = object | { default?: unknown; };

const require = createRequire(import.meta.url);

/**
* A simple no-operation function
*/
Expand Down
3 changes: 1 addition & 2 deletions src/helpers/mockComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import {
createElement,
} from "react";

import type { ScrollViewMethods } from "../lib/Components/ScrollView";
import type { TextInputMethods } from "../lib/Components/TextInput";
import type { ScrollViewMethods, TextInputMethods } from "./nativeMethodsMock";
import type { NativeMethods } from "react-native";

export type AllNativeMethods = NativeMethods | ScrollViewMethods | TextInputMethods;
Expand Down
79 changes: 78 additions & 1 deletion src/helpers/nativeMethodsMock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { noop } from "./commons";

import type { NativeMethods } from "react-native";
import type { ElementRef } from "react";
import type { HostComponent, Image, NativeMethods, ScrollView, TextInput, View } from "react-native";

export type ImageMethods = Partial<typeof Image>;

export type ScrollViewMethods = NativeMethods | ScrollView & {
getInnerViewRef: () => ElementRef<typeof View> | null;
getNativeScrollRef: () => ElementRef<HostComponent<unknown>> | null;
};

export type TextInputMethods = NativeMethods | TextInput & {
getNativeRef: () => ElementRef<HostComponent<unknown>> | undefined;
};

export const nativeMethodsMock: NativeMethods = {
blur: noop,
Expand All @@ -11,3 +23,68 @@ export const nativeMethodsMock: NativeMethods = {
refs: { },
setNativeProps: noop,
};

export const imageMethodsMock: ImageMethods = {
getSize: noop,
getSizeWithHeaders: noop,
prefetch: () => Promise.resolve(false),
prefetchWithMetadata: () => Promise.resolve(false),
queryCache: () => Promise.resolve({ }),
resolveAssetSource: () => ({
height: 0,
scale: 0,
uri: "",
width: 0,
}),
};

export const scrollViewMethodsMock: ScrollViewMethods = {
...nativeMethodsMock,
flashScrollIndicators: noop,
getInnerViewNode: noop,
getInnerViewRef: () => null,
getNativeScrollRef: () => null,
getScrollResponder: () => ({
addListenerOn: noop,
componentWillMount: noop,
scrollResponderGetScrollableNode: noop,
scrollResponderHandleMomentumScrollBegin: noop,
scrollResponderHandleMomentumScrollEnd: noop,
scrollResponderHandleResponderGrant: noop,
scrollResponderHandleResponderReject: noop,
scrollResponderHandleResponderRelease: noop,
scrollResponderHandleScroll: noop,
scrollResponderHandleScrollBeginDrag: noop,
scrollResponderHandleScrollEndDrag: noop,
scrollResponderHandleScrollShouldSetResponder: () => false,
scrollResponderHandleStartShouldSetResponder: () => false,
scrollResponderHandleStartShouldSetResponderCapture: () => false,
scrollResponderHandleTerminationRequest: () => false,
scrollResponderHandleTouchEnd: noop,
scrollResponderHandleTouchMove: noop,
scrollResponderHandleTouchStart: noop,
scrollResponderInputMeasureAndScrollToKeyboard: noop,
scrollResponderIsAnimating: () => false,
scrollResponderKeyboardDidHide: noop,
scrollResponderKeyboardDidShow: noop,
scrollResponderKeyboardWillHide: noop,
scrollResponderKeyboardWillShow: noop,
scrollResponderScrollNativeHandleToKeyboard: noop,
scrollResponderScrollTo: noop,
scrollResponderTextInputFocusError: noop,
scrollResponderZoomTo: noop,
}),
getScrollableNode: noop,
scrollResponderScrollNativeHandleToKeyboard: noop,
scrollResponderZoomTo: noop,
scrollTo: noop,
scrollToEnd: noop,
};

export const textInputMethodsMock: TextInputMethods = {
...nativeMethodsMock,
clear: noop,
getNativeRef: () => undefined,
isFocused: () => false,
setSelection: noop,
};
18 changes: 1 addition & 17 deletions src/lib/Components/Image.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,10 @@
import { Image } from "react-native";

import { noop } from "../../helpers/commons";
import { mockComponent } from "../../helpers/mockComponent";
import { imageMethodsMock } from "../../helpers/nativeMethodsMock";

import type { ComponentClass } from "react";

export type ImageMethods = Partial<typeof Image>;

export const imageMethodsMock: ImageMethods = {
getSize: noop,
getSizeWithHeaders: noop,
prefetch: () => Promise.resolve(false),
prefetchWithMetadata: () => Promise.resolve(false),
queryCache: () => Promise.resolve({ }),
resolveAssetSource: () => ({
height: 0,
scale: 0,
uri: "",
width: 0,
}),
};

const Mock = mockComponent(Image as ComponentClass);

export const ImageMock = Object.assign(Mock, imageMethodsMock);
57 changes: 3 additions & 54 deletions src/lib/Components/ScrollView.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,8 @@
/* eslint-disable sort-keys */
import { type ElementRef, type PropsWithChildren, type ReactNode, createElement } from "react";
import { type HostComponent, type NativeMethods, ScrollView, View, requireNativeComponent } from "react-native";
import { type PropsWithChildren, type ReactNode, createElement } from "react";
import { ScrollView, View, requireNativeComponent } from "react-native";

import { noop } from "../../helpers/commons";
import { mockComponent } from "../../helpers/mockComponent";
import { nativeMethodsMock } from "../../helpers/nativeMethodsMock";

export type ScrollViewMethods = NativeMethods | ScrollView & {
getInnerViewRef: () => ElementRef<typeof View> | null;
getNativeScrollRef: () => ElementRef<HostComponent<unknown>> | null;
};

export const scrollViewMethodsMock: ScrollViewMethods = {
...nativeMethodsMock,
getScrollResponder: () => ({
addListenerOn: noop,
componentWillMount: noop,
scrollResponderGetScrollableNode: noop,
scrollResponderHandleMomentumScrollBegin: noop,
scrollResponderHandleMomentumScrollEnd: noop,
scrollResponderHandleResponderGrant: noop,
scrollResponderHandleResponderReject: noop,
scrollResponderHandleResponderRelease: noop,
scrollResponderHandleScroll: noop,
scrollResponderHandleScrollBeginDrag: noop,
scrollResponderHandleScrollEndDrag: noop,
scrollResponderHandleScrollShouldSetResponder: () => false,
scrollResponderHandleStartShouldSetResponder: () => false,
scrollResponderHandleStartShouldSetResponderCapture: () => false,
scrollResponderHandleTerminationRequest: () => false,
scrollResponderHandleTouchEnd: noop,
scrollResponderHandleTouchMove: noop,
scrollResponderHandleTouchStart: noop,
scrollResponderInputMeasureAndScrollToKeyboard: noop,
scrollResponderIsAnimating: () => false,
scrollResponderKeyboardDidHide: noop,
scrollResponderKeyboardDidShow: noop,
scrollResponderKeyboardWillHide: noop,
scrollResponderKeyboardWillShow: noop,
scrollResponderScrollNativeHandleToKeyboard: noop,
scrollResponderScrollTo: noop,
scrollResponderTextInputFocusError: noop,
scrollResponderZoomTo: noop,
}),
getScrollableNode: noop,
getInnerViewNode: noop,
getInnerViewRef: () => null,
getNativeScrollRef: () => null,
scrollTo: noop,
scrollToEnd: noop,
flashScrollIndicators: noop,
scrollResponderZoomTo: noop,
scrollResponderScrollNativeHandleToKeyboard: noop,

};
import { scrollViewMethodsMock } from "../../helpers/nativeMethodsMock";

const RCTScrollView = requireNativeComponent("RCTScrollView");
const BaseMock = mockComponent(ScrollView, scrollViewMethodsMock);
Expand Down
19 changes: 2 additions & 17 deletions src/lib/Components/TextInput.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,6 @@
import { type HostComponent, type NativeMethods, TextInput } from "react-native";
import { TextInput } from "react-native";

import { noop } from "../../helpers/commons";
import { mockComponent } from "../../helpers/mockComponent";
import { nativeMethodsMock } from "../../helpers/nativeMethodsMock";

import type { ElementRef } from "react";

export type TextInputMethods = NativeMethods | TextInput & {
getNativeRef: () => ElementRef<HostComponent<unknown>> | undefined;
};

export const textInputMethodsMock: TextInputMethods = {
...nativeMethodsMock,
clear: noop,
getNativeRef: () => undefined,
isFocused: () => false,
setSelection: noop,
};
import { textInputMethodsMock } from "../../helpers/nativeMethodsMock";

export const TextInputMock = mockComponent(TextInput, textInputMethodsMock);
2 changes: 1 addition & 1 deletion src/lib/babelRegister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import register from "@babel/register";

register({
cache: true,
only: [/node_modules[/\\](react-native|@react-native)[/\\]/],
only: [/[/\\]node_modules[/\\](react-native|@react-native)[/\\]/],
plugins: [
["extension-resolver", {
extensions: [
Expand Down
19 changes: 13 additions & 6 deletions src/lib/mockNative.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { createRequire } from "module";

import { match } from "ts-pattern";

import { replace } from "../helpers/commons";
import { type AllNativeMethods, mockComponent } from "../helpers/mockComponent";
import { nativeMethodsMock } from "../helpers/nativeMethodsMock";

import { type ImageMethods, imageMethodsMock } from "./Components/Image";
import { type ScrollViewMethods, scrollViewMethodsMock } from "./Components/ScrollView";
import { type TextInputMethods, textInputMethodsMock } from "./Components/TextInput";
import {
type ImageMethods,
type ScrollViewMethods,
type TextInputMethods,
imageMethodsMock,
nativeMethodsMock,
scrollViewMethodsMock,
textInputMethodsMock,
} from "../helpers/nativeMethodsMock";

import type { ComponentClass, PropsWithChildren } from "react";
import type { NativeMethods } from "react-native";
Expand All @@ -23,6 +28,8 @@ export type NativeKey = NativeBase
| "ScrollView"
| "TextInput";

const require = createRequire(import.meta.url);

const MOCKS: Set<NativeKey> = new Set();

const PATHS: Record<NativeKey, string> = {
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { mockNative, restoreNativeMocks } from "./lib/mockNative";

export type { NativeBase, NativeKey } from "./lib/mockNative";
4 changes: 3 additions & 1 deletion src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { createRequire } from "module";
import pino from "pino";
import pinoPretty from "pino-pretty";

import { name } from "../package.json";

const start = Date.now();
const logger = pino(pinoPretty({ colorize: true }));
const logger = pino({ name }, pinoPretty({ colorize: true }));
const require = createRequire(import.meta.url);

require("./load.cjs");
Expand Down
15 changes: 15 additions & 0 deletions src/vitest/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { name } from "../../package.json";

import type { Environment } from "vitest";

const reactNativeEnv: Environment = {
name: "react-native",
setup: async () => {
await import(`${name}/register`);

return { teardown: () => undefined };
},
transformMode: "ssr",
};

export default reactNativeEnv;
Loading

0 comments on commit 45b6821

Please sign in to comment.