├── .gitignore
├── pnpm-workspace.yaml
├── packages
└── vitest-react-native
│ ├── setup.d.ts
│ ├── plugin.d.ts
│ ├── package.json
│ ├── plugin.js
│ └── setup.js
├── test
├── src
│ ├── Platform.ios.jsx
│ ├── Item.jsx
│ └── itemStyles.js
├── __snapshots__
│ ├── PlatformSpecific.spec.jsx.snap
│ ├── ScrollView.spec.jsx.snap
│ ├── Modal.spec.jsx.snap
│ ├── nativeComponent.spec.jsx.snap
│ └── Navigator.spec.jsx.snap
├── nativeComponent.spec.jsx
├── PlatformSpecific.spec.jsx
├── Modal.spec.jsx
├── ScrollView.spec.jsx
├── assets
│ └── trashIcon.svg
├── native-tags.spec.jsx
└── Navigator.spec.jsx
├── vitest.config.js
├── example
├── index.jsx
└── index.test.jsx
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - packages/*
3 |
--------------------------------------------------------------------------------
/packages/vitest-react-native/setup.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/packages/vitest-react-native/plugin.d.ts:
--------------------------------------------------------------------------------
1 | declare function plugin(): import("vite").Plugin;
2 | export default plugin;
3 |
--------------------------------------------------------------------------------
/test/src/Platform.ios.jsx:
--------------------------------------------------------------------------------
1 | import { Text } from 'react-native'
2 |
3 | export const Platform = () => {
4 | return iOS
5 | }
6 |
--------------------------------------------------------------------------------
/test/__snapshots__/PlatformSpecific.spec.jsx.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`platform specific renders correctly 1`] = `
4 |
5 | iOS
6 |
7 | `;
8 |
--------------------------------------------------------------------------------
/vitest.config.js:
--------------------------------------------------------------------------------
1 | const react = require("@vitejs/plugin-react");
2 | const reactNative = require("./packages/vitest-react-native/plugin");
3 | const { defineConfig } = require("vitest/config");
4 |
5 | module.exports = defineConfig({
6 | plugins: [reactNative(), react()],
7 | });
8 |
--------------------------------------------------------------------------------
/test/nativeComponent.spec.jsx:
--------------------------------------------------------------------------------
1 | import { test, expect } from 'vitest'
2 | import { render } from '@testing-library/react-native'
3 | import Item from './src/Item'
4 |
5 | test('native components render correctly', () => {
6 | const container = render( )
7 | expect(container).toMatchSnapshot()
8 | })
--------------------------------------------------------------------------------
/test/__snapshots__/ScrollView.spec.jsx.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`scroll view components render correctly 1`] = `
4 |
5 |
6 |
7 | Hello, world!
8 |
9 |
10 |
11 | `;
12 |
--------------------------------------------------------------------------------
/test/PlatformSpecific.spec.jsx:
--------------------------------------------------------------------------------
1 | import { Platform } from './src/Platform';
2 | import { test, expect } from 'vitest'
3 | import { render } from '@testing-library/react-native'
4 |
5 | test('platform specific renders correctly', () => {
6 | const app = render()
7 | expect(app).toMatchSnapshot()
8 | })
9 |
--------------------------------------------------------------------------------
/test/__snapshots__/Modal.spec.jsx.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`scroll view components render correctly 1`] = `
4 |
8 |
9 | Hello, world!
10 |
11 |
12 | `;
13 |
--------------------------------------------------------------------------------
/example/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | export const HelloWorldApp = () => {
5 | return (
6 |
12 | Hello, world!
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/test/Modal.spec.jsx:
--------------------------------------------------------------------------------
1 | import { test, expect } from 'vitest'
2 | import { render } from '@testing-library/react-native'
3 | import { Modal, Text } from 'react-native'
4 |
5 | const View = () => {
6 | return (
7 |
8 | Hello, world!
9 |
10 | )
11 | }
12 |
13 | test('scroll view components render correctly', () => {
14 | const container = render()
15 | expect(container).toMatchSnapshot()
16 | })
--------------------------------------------------------------------------------
/test/ScrollView.spec.jsx:
--------------------------------------------------------------------------------
1 | import { test, expect } from 'vitest'
2 | import { render } from '@testing-library/react-native'
3 | import { ScrollView, Text } from 'react-native'
4 |
5 | const View = () => {
6 | return (
7 |
8 | Hello, world!
9 |
10 | )
11 | }
12 |
13 | test('scroll view components render correctly', () => {
14 | const container = render()
15 | expect(container).toMatchSnapshot()
16 | })
--------------------------------------------------------------------------------
/test/src/Item.jsx:
--------------------------------------------------------------------------------
1 | import { View, Pressable } from 'react-native';
2 | import { styles } from './itemStyles';
3 | import TrashIcon from '../assets/trashIcon.svg';
4 |
5 | const Item = (props) => {
6 | return (
7 |
8 | props.trashTodo(props.id)}
14 | hitSlop={10}
15 | >
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default Item
--------------------------------------------------------------------------------
/example/index.test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { test, expect } from "vitest";
3 | import { HelloWorldApp } from "./index";
4 | import * as renderer from "@testing-library/react-native";
5 |
6 | test("HelloWorldApp", () => {
7 | const view = renderer.render();
8 | expect(view.getByText(/Hello/)).toBeTruthy();
9 | expect(view.toJSON()).toMatchInlineSnapshot(`
10 |
19 |
20 | Hello, world!
21 |
22 |
23 | `);
24 | });
25 |
--------------------------------------------------------------------------------
/test/assets/trashIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "monorepo",
3 | "private": true,
4 | "version": "0.1.5",
5 | "scripts": {
6 | "test": "vitest",
7 | "release": "pnpm -r publish --access public"
8 | },
9 | "devDependencies": {
10 | "@react-navigation/bottom-tabs": "6.5.8",
11 | "@react-navigation/native": "6.1.7",
12 | "@react-navigation/native-stack": "6.9.13",
13 | "@rneui/base": "4.0.0-rc.7",
14 | "@testing-library/react-native": "12.0.0-rc.0",
15 | "@vitejs/plugin-react": "^3.1.0",
16 | "jsdom": "22.1.0",
17 | "react": "18.2.0",
18 | "react-native": "^0.71.3",
19 | "react-native-svg": "^13.9.0",
20 | "react-native-web": "0.18.2",
21 | "react-test-renderer": "^18.2.0",
22 | "vite": "^4.3.9",
23 | "vitest": "^0.31.4"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/vitest-react-native/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vitest-react-native",
3 | "version": "0.1.5",
4 | "description": "Support for running React Native inside Vitest",
5 | "main": "plugin.js",
6 | "scripts": {
7 | "test": "vitest"
8 | },
9 | "author": "Vladimir Sheremet",
10 | "files": [
11 | "plugin.js",
12 | "setup.js",
13 | "*.d.ts"
14 | ],
15 | "exports": {
16 | ".": "./plugin.js",
17 | "./setup": "./setup.js"
18 | },
19 | "license": "ISC",
20 | "peerDependencies": {
21 | "react": "*",
22 | "vite": "*",
23 | "react-native": "*"
24 | },
25 | "dependencies": {
26 | "@react-native/polyfills": "^2.0.0",
27 | "@bunchtogether/vite-plugin-flow": "^1.0.2",
28 | "esbuild": "^0.17.10",
29 | "flow-remove-types": "^2.200.0",
30 | "pirates": "^4.0.5",
31 | "regenerator-runtime": "^0.13.11"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vitest-react-native
2 |
3 | > **Warning**
4 | > This package is still WIP. If you encounter any errors, feel free to open an issue or a pull request.
5 |
6 | ## Installing
7 |
8 | To add support for `react-native` to Vitest, you need to install this plugin and add it to your Vitest configuration file.
9 |
10 | ```shell
11 | # with npm
12 | npm install vitest-react-native -D
13 |
14 | # with yarn
15 | yarn add vitest-react-native -D
16 |
17 | # with pnpm
18 | pnpm add vitest-react-native -D
19 |
20 | # with bun
21 | bun add vitest-react-native -D
22 | ```
23 |
24 | ## Usage
25 |
26 | ```js
27 | // vitest.config.mjs
28 | import reactNative from "vitest-react-native";
29 | // this is needed for react jsx support
30 | import react from "@vitejs/plugin-react";
31 | import { defineConfig } from "vitest/config";
32 |
33 | export default defineConfig({
34 | plugins: [reactNative(), react()],
35 | });
36 | ```
37 |
--------------------------------------------------------------------------------
/packages/vitest-react-native/plugin.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("path");
2 |
3 | module.exports = () => {
4 | /** @type {import('vite').Plugin} */
5 | const plugin = {
6 | name: "vitest-plugin-react-native",
7 | config: () => {
8 | return {
9 | resolve: {
10 | extensions: [
11 | '.ios.js',
12 | '.ios.jsx',
13 | '.ios.ts',
14 | '.ios.tsx',
15 | '.mjs',
16 | '.js',
17 | '.mts',
18 | '.ts',
19 | '.jsx',
20 | '.tsx',
21 | '.json'
22 | ],
23 | conditions: ["react-native"],
24 | },
25 | test: {
26 | setupFiles: [resolve(__dirname, "setup.js")],
27 | globals: true,
28 | server: {
29 | deps: {
30 | external: ["react-native"],
31 | },
32 | },
33 | },
34 | };
35 | },
36 | };
37 | return plugin
38 | };
39 |
--------------------------------------------------------------------------------
/test/src/itemStyles.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | export const styles = StyleSheet.create({
4 | itemContainer: {
5 | flexDirection: 'row',
6 | alignItems: 'center',
7 | paddingTop: 10,
8 | paddingBottom: 15,
9 | paddingHorizontal: 15,
10 | backgroundColor: '#f7f8fa',
11 | },
12 | itemCheckbox: {
13 | justifyContent: 'center',
14 | alignItems: 'center',
15 | width: 20,
16 | height: 20,
17 | marginRight: 13,
18 | borderRadius: 6,
19 | },
20 | itemCheckboxCheckedIcon: {
21 | shadowColor: '#000000',
22 | shadowOpacity: 0.14,
23 | shadowRadius: 8,
24 | shadowOffset: {
25 | width: 0,
26 | height: 4,
27 | },
28 | },
29 | itemText: {
30 | marginRight: 'auto',
31 | paddingRight: 25,
32 | fontSize: 15,
33 | lineHeight: 20,
34 | color: '#737373',
35 | },
36 | itemTextChecked: {
37 | opacity: 0.3,
38 | textDecorationLine: 'line-through',
39 | },
40 | trashButton: {
41 | opacity: 0.8,
42 | },
43 | trashButtonDone: {
44 | opacity: 0.3,
45 | },
46 | });
47 |
--------------------------------------------------------------------------------
/test/native-tags.spec.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { test, expect } from "vitest"
3 | import { Image, Modal, View } from "react-native"
4 | import { render } from "@testing-library/react-native"
5 | import { Svg } from "react-native-svg"
6 | // import { Divider } from "@rneui/base"
7 |
8 | // Not working
9 | test("Image", () => {
10 | const { getByTestId } = render()
11 | expect(getByTestId("test")).not.toBeNull()
12 | })
13 |
14 | // Working
15 | test("Modal", () => {
16 | const { getByTestId } = render()
17 | expect(getByTestId("test")).not.toBeNull()
18 | })
19 |
20 | // Not working
21 | // test("FlatList", () => {
22 | // const { getByTestId } = render(
23 | // } />
24 | // )
25 | // expect(getByTestId("test")).not.toBeNull()
26 | // })
27 |
28 | // Working
29 | test("react-native-svg", () => {
30 | const { getByTestId } = render(
31 |
32 |
33 |
34 | )
35 | expect(getByTestId("test")).not.toBeNull()
36 | })
37 |
38 | // Not working
39 | // test("react-native-elements", () => {
40 | // const { getByTestId } = render(
41 | //
42 | //
43 | //
44 | // )
45 | // expect(getByTestId("test")).not.toBeNull()
46 | // })
--------------------------------------------------------------------------------
/test/Navigator.spec.jsx:
--------------------------------------------------------------------------------
1 | // @vitest-environment jsdom
2 | import { NavigationContainer } from '@react-navigation/native'
3 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
4 | import { createNativeStackNavigator } from '@react-navigation/native-stack'
5 | import { View, Text } from 'react-native'
6 | import { test, expect } from 'vitest'
7 | import { render } from '@testing-library/react-native'
8 |
9 | const App = () => {
10 | const TabNavigator = createBottomTabNavigator()
11 | const StackNavigator = createNativeStackNavigator()
12 |
13 | const Home = () => (
14 |
15 |
16 |
17 |
18 | )
19 | const Card1 = () => (Card 1)
20 | const Card2 = () => (Card 2)
21 | const Profile = () => (Profile)
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | test('navigator renders correctly', () => {
34 | const app = render()
35 | expect(app).toMatchSnapshot()
36 | })
37 |
--------------------------------------------------------------------------------
/test/__snapshots__/nativeComponent.spec.jsx.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`native components render correctly 1`] = `
4 |
16 |
56 |
59 |
60 |
61 | `;
62 |
--------------------------------------------------------------------------------
/packages/vitest-react-native/setup.js:
--------------------------------------------------------------------------------
1 | const addHook = require("pirates").addHook;
2 | const removeTypes = require("flow-remove-types");
3 | const esbuild = require("esbuild");
4 | const fs = require('fs')
5 | const os = require('os')
6 | const path = require('path')
7 | const reactNativePkg = require('react-native/package.json')
8 | const pluginPkg = require('./package.json')
9 |
10 | const tmpDir = os.tmpdir()
11 | const cacheDirBase = path.join(tmpDir, 'vrn')
12 | const version = reactNativePkg.version + pluginPkg.version
13 | const cacheDir = path.join(cacheDirBase, version)
14 | if (!fs.existsSync(cacheDir)) {
15 | fs.mkdirSync(cacheDir, { recursive: true })
16 | }
17 | const cacheDirFolders = fs.readdirSync(cacheDirBase)
18 | cacheDirFolders.forEach(version => {
19 | // remove old cache
20 | if (version !== version) {
21 | fs.rmdirSync(path.join(cacheDirBase, version))
22 | }
23 | })
24 |
25 | const root = process.cwd()
26 |
27 | const mocked = [];
28 | // TODO: better check
29 | const getMocked = (path) => mocked.find(([p]) => path.includes(p));
30 |
31 | const crossPlatformFiles = [
32 | "Settings",
33 | "BaseViewConfig",
34 | "RCTAlertManager",
35 | "PlatformColorValueTypes",
36 | "PlatformColorValueTypesIOS",
37 | "PlatformColorValueTypesIOS",
38 | "RCTNetworking",
39 | "Image",
40 | "Platform",
41 | "LoadingView",
42 | "LoadingView",
43 | "BackHandler",
44 | "ProgressViewIOS",
45 | "ProgressBarAndroid",
46 | "legacySendAccessibilityEvent",
47 | "DatePickerIOS",
48 | "DatePickerIOS.flow",
49 | "DrawerLayoutAndroid",
50 | "ToastAndroid",
51 | ];
52 |
53 | // we need to process react-native dependency, because they ship flow types
54 | // removing types is not enough, we also need to convert ESM imports/exports into CJS
55 | const transformCode = (code) => {
56 | const result = removeTypes(code).toString();
57 | return esbuild
58 | .transformSync(result, {
59 | loader: "jsx",
60 | format: "cjs",
61 | platform: "node",
62 | })
63 | .code;
64 | };
65 |
66 | const normalize = (path) => path.replace(/\\/g, "/");
67 |
68 | const cacheExists = (cachePath) => fs.existsSync(cachePath)
69 | const readFromCache = (cachePath) => fs.readFileSync(cachePath, 'utf-8')
70 | const writeToCache = (cachePath, code) => fs.writeFileSync(cachePath, code)
71 |
72 | const processBinary = (code, filename) => {
73 | const b64 = Buffer.from(code).toString('base64')
74 | return `module.exports = Buffer.from("${b64}", "base64")`
75 | }
76 |
77 | addHook(
78 | (code, filename) => {
79 | return processBinary(code, filename)
80 | },
81 | {
82 | exts: [".png", ".jpg"],
83 | ignoreNodeModules: false
84 | }
85 | )
86 |
87 | require.extensions['.ios.js'] = require.extensions['.js']
88 |
89 | const processReactNative = (code, filename) => {
90 | const cacheName = normalize(path.relative(root, filename)).replace(/\//g, '_')
91 | const cachePath = path.join(cacheDir, cacheName)
92 | if (cacheExists(cachePath))
93 | return readFromCache(cachePath, 'utf-8')
94 | const mock = getMocked(filename);
95 | if (mock) {
96 | const original = mock[1].includes("__vitest__original__")
97 | ? `const __vitest__original__ = ((module, exports) => {
98 | ${transformCode(code)}
99 | return module.exports
100 | })(module, exports);`
101 | : "";
102 | const mockCode = `
103 | ${original}
104 | ${mock[1]}
105 | `;
106 | writeToCache(cachePath, mockCode)
107 | return mockCode;
108 | }
109 | const transformed = transformCode(code);
110 | writeToCache(cachePath, transformed)
111 | return transformed
112 | }
113 |
114 | addHook(
115 | (code, filename) => {
116 | return processReactNative(code, filename)
117 | },
118 | {
119 | exts: [".js", ".ios.js"],
120 | ignoreNodeModules: false,
121 | matcher: (id) => {
122 | const path = normalize(id)
123 | return (
124 | (path.includes("/node_modules/react-native/")
125 | || path.includes("/node_modules/@react-native/"))
126 | // renderer doesn't have jsx inside and it's too big to process
127 | && !path.includes('Renderer/implementations')
128 | )
129 | }
130 | }
131 | );
132 |
133 | // adapted from https://github.com/facebook/react-native/blob/main/jest/setup.js
134 |
135 | require("@react-native/polyfills/Object.es8");
136 | // require("@react-native/polyfills/error-guard");
137 |
138 | Object.defineProperties(globalThis, {
139 | __DEV__: {
140 | configurable: true,
141 | enumerable: true,
142 | value: true,
143 | writable: true,
144 | },
145 | cancelAnimationFrame: {
146 | configurable: true,
147 | enumerable: true,
148 | value: id => clearTimeout(id),
149 | writable: true,
150 | },
151 | performance: {
152 | configurable: true,
153 | enumerable: true,
154 | value: {
155 | now: vi.fn(Date.now),
156 | },
157 | writable: true,
158 | },
159 | regeneratorRuntime: {
160 | configurable: true,
161 | enumerable: true,
162 | value: require('regenerator-runtime/runtime'),
163 | writable: true,
164 | },
165 | ensureNativeMethodsAreSynced: {
166 | configurable: true,
167 | enumerable: true,
168 | value: {
169 | now: vi.fn(),
170 | },
171 | writable: true,
172 | },
173 | requestAnimationFrame: {
174 | configurable: true,
175 | enumerable: true,
176 | value: callback => setTimeout(() => callback(vi.getRealSystemTime()), 0),
177 | writable: true,
178 | },
179 | window: {
180 | configurable: true,
181 | enumerable: true,
182 | value: global,
183 | writable: true,
184 | },
185 | })
186 |
187 | const mock = (path, mock) => {
188 | if (typeof mock !== "function") {
189 | throw new Error(
190 | `mock must be a function, got ${typeof mock} instead for ${path}`
191 | );
192 | }
193 | mocked.push([path, `module.exports = ${mock()}`]);
194 | };
195 |
196 | const mockComponent = (moduleName, instanceMethods, isESModule = false, customSetup = '') => {
197 | return `(() => {const RealComponent = ${isESModule}
198 | ? __vitest__original__.default
199 | : __vitest__original__;
200 | const React = require('react');
201 |
202 | const SuperClass =
203 | typeof RealComponent === 'function' ? RealComponent : React.Component;
204 |
205 | const name =
206 | RealComponent.displayName ||
207 | RealComponent.name ||
208 | (RealComponent.render // handle React.forwardRef
209 | ? RealComponent.render.displayName || RealComponent.render.name
210 | : 'Unknown');
211 |
212 | const nameWithoutPrefix = name.replace(/^(RCT|RK)/, '');
213 |
214 | const Component = class extends SuperClass {
215 | static displayName = 'Component';
216 |
217 | render() {
218 | const props = Object.assign({}, RealComponent.defaultProps);
219 |
220 | if (this.props) {
221 | Object.keys(this.props).forEach(prop => {
222 | // We can't just assign props on top of defaultProps
223 | // because React treats undefined as special and different from null.
224 | // If a prop is specified but set to undefined it is ignored and the
225 | // default prop is used instead. If it is set to null, then the
226 | // null value overwrites the default value.
227 | if (this.props[prop] !== undefined) {
228 | props[prop] = this.props[prop];
229 | }
230 | });
231 | }
232 |
233 | return React.createElement(nameWithoutPrefix, props, this.props.children);
234 | }
235 | };
236 |
237 | Component.displayName = nameWithoutPrefix;
238 |
239 | Object.keys(RealComponent).forEach(classStatic => {
240 | Component[classStatic] = RealComponent[classStatic];
241 | });
242 |
243 | ${
244 | instanceMethods
245 | ? `Object.assign(Component.prototype, ${instanceMethods});`
246 | : ""
247 | }
248 |
249 | ${customSetup}
250 |
251 | return Component;
252 | })()`;
253 | };
254 |
255 | const mockModal = () => {
256 | return `((BaseComponent) => {
257 | const React = require('react')
258 | ${transformCode(`class ModalMock extends BaseComponent {
259 | render() {
260 | return (
261 |
262 | {this.props.visible !== true ? null : this.props.children}
263 |
264 | );
265 | }
266 | }`)}
267 | return ModalMock;
268 | })
269 | `;
270 | };
271 |
272 | const mockScrollView = () => {
273 | return `((BaseComponent) => {
274 | const requireNativeComponent = require("react-native/Libraries/ReactNative/requireNativeComponent");
275 | const RCTScrollView = requireNativeComponent('RCTScrollView');
276 | const React = require('react')
277 | const View = require('react-native/Libraries/Components/View/View')
278 | return ${transformCode(`class ScrollViewMock extends BaseComponent {
279 | render() {
280 | return (
281 |
282 | {this.props.refreshControl}
283 | {this.props.children}
284 |
285 | );
286 | }
287 | }`)}
288 | })`;
289 | };
290 |
291 | const MockNativeMethods = `
292 | measure: vi.fn(),
293 | measureInWindow: vi.fn(),
294 | measureLayout: vi.fn(),
295 | setNativeProps: vi.fn(),
296 | focus: vi.fn(),
297 | blur: vi.fn(),
298 | `;
299 |
300 | mock("react-native/Libraries/Core/InitializeCore", () => "{}");
301 | mock(
302 | "react-native/Libraries/Core/NativeExceptionsManager",
303 | () => `{
304 | __esModule: true,
305 | default: {
306 | reportfatalexception: vi.fn(),
307 | reportSoftException: vi.fn(),
308 | updateExceptionMessage: vi.fn(),
309 | dismissRedbox: vi.fn(),
310 | reportException: vi.fn(),
311 | }
312 | }`
313 | );
314 | mock(
315 | "react-native/Libraries/ReactNative/UIManager",
316 | () => `{
317 | AndroidViewPager: {
318 | Commands: {
319 | setPage: vi.fn(),
320 | setPageWithoutAnimation: vi.fn(),
321 | },
322 | },
323 | blur: vi.fn(),
324 | createView: vi.fn(),
325 | customBubblingEventTypes: {},
326 | customDirectEventTypes: {},
327 | dispatchViewManagerCommand: vi.fn(),
328 | focus: vi.fn(),
329 | getViewManagerConfig: vi.fn((name) => {
330 | if (name === "AndroidDrawerLayout") {
331 | return {
332 | Constants: {
333 | DrawerPosition: {
334 | Left: 10,
335 | },
336 | },
337 | };
338 | }
339 | }),
340 | hasViewManagerConfig: vi.fn((name) => {
341 | return name === "AndroidDrawerLayout";
342 | }),
343 | measure: vi.fn(),
344 | manageChildren: vi.fn(),
345 | removeSubviewsFromContainerWithID: vi.fn(),
346 | replaceExistingNonRootView: vi.fn(),
347 | setChildren: vi.fn(),
348 | updateView: vi.fn(),
349 | AndroidDrawerLayout: {
350 | Constants: {
351 | DrawerPosition: {
352 | Left: 10,
353 | },
354 | },
355 | },
356 | AndroidTextInput: {
357 | Commands: {},
358 | },
359 | ScrollView: {
360 | Constants: {},
361 | },
362 | View: {
363 | Constants: {},
364 | },
365 | }`
366 | );
367 | mock("react-native/Libraries/Image/Image", () => {
368 | return mockComponent("react-native/Libraries/Image/Image", '', false, `
369 | Component.getSize = vi.fn();
370 | Component.getSizeWithHeaders = vi.fn();
371 | Component.prefetch = vi.fn();
372 | Component.prefetchWithMetadata = vi.fn();
373 | Component.queryCache = vi.fn();
374 | Component.resolveAssetSource = vi.fn();
375 | `)
376 | });
377 | mock("react-native/Libraries/Text/Text", () =>
378 | mockComponent("react-native/Libraries/Text/Text", `{ ${MockNativeMethods} }`)
379 | );
380 | mock("react-native/Libraries/Components/TextInput/TextInput", () =>
381 | mockComponent(
382 | "react-native/Libraries/Components/TextInput/TextInput",
383 | `{
384 | ${MockNativeMethods}
385 | isFocused: vi.fn(),
386 | clear: vi.fn(),
387 | getNativeRef: vi.fn(),
388 | }`
389 | )
390 | );
391 |
392 | mock("react-native/Libraries/Modal/Modal", () => {
393 | const component = mockComponent("react-native/Libraries/Modal/Modal");
394 | return `${mockModal()}(${component});`;
395 | });
396 |
397 | mock("react-native/Libraries/Components/View/View", () =>
398 | mockComponent(
399 | "react-native/Libraries/Components/View/View",
400 | `{ ${MockNativeMethods} }`
401 | )
402 | );
403 |
404 | mock(
405 | "react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo",
406 | () => `{
407 | __esModule: true,
408 | default: {
409 | addEventListener: vi.fn(),
410 | announceForAccessibility: vi.fn(),
411 | isAccessibilityServiceEnabled: vi.fn(),
412 | isBoldTextEnabled: vi.fn(),
413 | isGrayscaleEnabled: vi.fn(),
414 | isInvertColorsEnabled: vi.fn(),
415 | isReduceMotionEnabled: vi.fn(),
416 | prefersCrossFadeTransitions: vi.fn(),
417 | isReduceTransparencyEnabled: vi.fn(),
418 | isScreenReaderEnabled: vi.fn(() => Promise.resolve(false)),
419 | setAccessibilityFocus: vi.fn(),
420 | sendAccessibilityEvent: vi.fn(),
421 | getRecommendedTimeoutMillis: vi.fn(),
422 | },
423 | }`
424 | );
425 |
426 | mock(
427 | "react-native/Libraries/Components/Clipboard/Clipboard",
428 | () => `{
429 | getString: vi.fn(() => ""),
430 | setString: vi.fn(),
431 | }`
432 | );
433 |
434 | mock(
435 | "react-native/Libraries/Components/RefreshControl/RefreshControl",
436 | () =>
437 | `require("react-native/Libraries/Components/RefreshControl/__mocks__/RefreshControlMock")`
438 | );
439 |
440 | mock("react-native/Libraries/Components/ScrollView/ScrollView", () => {
441 | const component = mockComponent(
442 | "react-native/Libraries/Components/ScrollView/ScrollView",
443 | `{
444 | ${MockNativeMethods}
445 | getScrollResponder: vi.fn(),
446 | getScrollableNode: vi.fn(),
447 | getInnerViewNode: vi.fn(),
448 | getInnerViewRef: vi.fn(),
449 | getNativeScrollRef: vi.fn(),
450 | scrollTo: vi.fn(),
451 | scrollToEnd: vi.fn(),
452 | flashScrollIndicators: vi.fn(),
453 | scrollResponderZoomTo: vi.fn(),
454 | scrollResponderScrollNativeHandleToKeyboard: vi.fn(),
455 | }`
456 | );
457 | return `${mockScrollView()}(${component});`;
458 | });
459 |
460 | mock(
461 | "react-native/Libraries/Components/ActivityIndicator/ActivityIndicator",
462 | () => `{
463 | __esModule: true,
464 | default: ${mockComponent(
465 | "react-native/Libraries/Components/ActivityIndicator/ActivityIndicator",
466 | null,
467 | true
468 | )},
469 | }
470 | `
471 | );
472 |
473 | mock(
474 | "react-native/Libraries/AppState/AppState",
475 | () => `{
476 | addEventListener: vi.fn(() => ({
477 | remove: vi.fn(),
478 | })),
479 | }`
480 | );
481 |
482 | mock(
483 | "react-native/Libraries/Linking/Linking",
484 | () => `{
485 | openURL: vi.fn(),
486 | canOpenURL: vi.fn(() => Promise.resolve(true)),
487 | openSettings: vi.fn(),
488 | addEventListener: vi.fn(),
489 | getInitialURL: vi.fn(() => Promise.resolve()),
490 | sendIntent: vi.fn(),
491 | }`
492 | );
493 |
494 | mock(
495 | "react-native/Libraries/BatchedBridge/NativeModules",
496 | () => `{
497 | AlertManager: {
498 | alertWithArgs: vi.fn(),
499 | },
500 | AsyncLocalStorage: {
501 | multiGet: vi.fn((keys, callback) =>
502 | process.nextTick(() => callback(null, []))
503 | ),
504 | multiSet: vi.fn((entries, callback) =>
505 | process.nextTick(() => callback(null))
506 | ),
507 | multiRemove: vi.fn((keys, callback) =>
508 | process.nextTick(() => callback(null))
509 | ),
510 | multiMerge: vi.fn((entries, callback) =>
511 | process.nextTick(() => callback(null))
512 | ),
513 | clear: vi.fn((callback) => process.nextTick(() => callback(null))),
514 | getAllKeys: vi.fn((callback) =>
515 | process.nextTick(() => callback(null, []))
516 | ),
517 | },
518 | DeviceInfo: {
519 | getConstants() {
520 | return {
521 | Dimensions: {
522 | window: {
523 | fontScale: 2,
524 | height: 1334,
525 | scale: 2,
526 | width: 750,
527 | },
528 | screen: {
529 | fontScale: 2,
530 | height: 1334,
531 | scale: 2,
532 | width: 750,
533 | },
534 | },
535 | };
536 | },
537 | },
538 | DevSettings: {
539 | addMenuItem: vi.fn(),
540 | reload: vi.fn(),
541 | },
542 | ImageLoader: {
543 | getSize: vi.fn((url) => Promise.resolve([320, 240])),
544 | prefetchImage: vi.fn(),
545 | },
546 | ImageViewManager: {
547 | getSize: vi.fn((uri, success) =>
548 | process.nextTick(() => success(320, 240))
549 | ),
550 | prefetchImage: vi.fn(),
551 | },
552 | KeyboardObserver: {
553 | addListener: vi.fn(),
554 | removeListeners: vi.fn(),
555 | },
556 | Networking: {
557 | sendRequest: vi.fn(),
558 | abortRequest: vi.fn(),
559 | addListener: vi.fn(),
560 | removeListeners: vi.fn(),
561 | },
562 | PlatformConstants: {
563 | getConstants() {
564 | return {};
565 | },
566 | },
567 | PushNotificationManager: {
568 | presentLocalNotification: vi.fn(),
569 | scheduleLocalNotification: vi.fn(),
570 | cancelAllLocalNotifications: vi.fn(),
571 | removeAllDeliveredNotifications: vi.fn(),
572 | getDeliveredNotifications: vi.fn((callback) =>
573 | process.nextTick(() => [])
574 | ),
575 | removeDeliveredNotifications: vi.fn(),
576 | setApplicationIconBadgeNumber: vi.fn(),
577 | getApplicationIconBadgeNumber: vi.fn((callback) =>
578 | process.nextTick(() => callback(0))
579 | ),
580 | cancelLocalNotifications: vi.fn(),
581 | getScheduledLocalNotifications: vi.fn((callback) =>
582 | process.nextTick(() => callback())
583 | ),
584 | requestPermissions: vi.fn(() =>
585 | Promise.resolve({ alert: true, badge: true, sound: true })
586 | ),
587 | abandonPermissions: vi.fn(),
588 | checkPermissions: vi.fn((callback) =>
589 | process.nextTick(() =>
590 | callback({ alert: true, badge: true, sound: true })
591 | )
592 | ),
593 | getInitialNotification: vi.fn(() => Promise.resolve(null)),
594 | addListener: vi.fn(),
595 | removeListeners: vi.fn(),
596 | },
597 | SourceCode: {
598 | getConstants() {
599 | return {
600 | scriptURL: null,
601 | };
602 | },
603 | },
604 | StatusBarManager: {
605 | setColor: vi.fn(),
606 | setStyle: vi.fn(),
607 | setHidden: vi.fn(),
608 | setNetworkActivityIndicatorVisible: vi.fn(),
609 | setBackgroundColor: vi.fn(),
610 | setTranslucent: vi.fn(),
611 | getConstants: () => ({
612 | HEIGHT: 42,
613 | }),
614 | },
615 | Timing: {
616 | createTimer: vi.fn(),
617 | deleteTimer: vi.fn(),
618 | },
619 | UIManager: {},
620 | BlobModule: {
621 | getConstants: () => ({ BLOB_URI_SCHEME: "content", BLOB_URI_HOST: null }),
622 | addNetworkingHandler: vi.fn(),
623 | enableBlobSupport: vi.fn(),
624 | disableBlobSupport: vi.fn(),
625 | createFromParts: vi.fn(),
626 | sendBlob: vi.fn(),
627 | release: vi.fn(),
628 | },
629 | WebSocketModule: {
630 | connect: vi.fn(),
631 | send: vi.fn(),
632 | sendBinary: vi.fn(),
633 | ping: vi.fn(),
634 | close: vi.fn(),
635 | addListener: vi.fn(),
636 | removeListeners: vi.fn(),
637 | },
638 | I18nManager: {
639 | allowRTL: vi.fn(),
640 | forceRTL: vi.fn(),
641 | swapLeftAndRightInRTL: vi.fn(),
642 | getConstants: () => ({
643 | isRTL: false,
644 | doLeftAndRightSwapInRTL: true,
645 | }),
646 | },
647 | }`
648 | );
649 |
650 | mock(
651 | "react-native/Libraries/NativeComponent/NativeComponentRegistry",
652 | () => `{
653 | get: vi.fn((name, viewConfigProvider) => {
654 | const requireNativeComponent = require("react-native/Libraries/ReactNative/requireNativeComponent");
655 | return requireNativeComponent(name);
656 | }),
657 | getWithFallback_DEPRECATED: vi.fn((name, viewConfigProvider) => {
658 | const requireNativeComponent = require("react-native/Libraries/ReactNative/requireNativeComponent");
659 | return requireNativeComponent(name);
660 | }),
661 | setRuntimeConfigProvider: vi.fn(),
662 | }`
663 | );
664 |
665 | mock(
666 | "react-native/Libraries/ReactNative/requireNativeComponent",
667 | () => `(() => {
668 | const React = require('react')
669 |
670 | let nativeTag = 1
671 |
672 | return viewName => {
673 | const Component = class extends React.Component {
674 | _nativeTag = nativeTag++;
675 |
676 | render() {
677 | return React.createElement(viewName, this.props, this.props.children);
678 | }
679 |
680 | // The methods that exist on host components
681 | blur = vi.fn();
682 | focus = vi.fn();
683 | measure = vi.fn();
684 | measureInWindow = vi.fn();
685 | measureLayout = vi.fn();
686 | setNativeProps = vi.fn();
687 | };
688 |
689 | if (viewName === 'RCTView') {
690 | Component.displayName = 'View';
691 | } else {
692 | Component.displayName = viewName;
693 | }
694 |
695 | return Component;
696 | };
697 | })()`
698 | );
699 |
700 | mock(
701 | "react-native/Libraries/Utilities/verifyComponentAttributeEquivalence",
702 | () => `() => {}`
703 | );
704 | mock(
705 | "react-native/Libraries/Vibration/Vibration",
706 | () => `{
707 | vibrate: vi.fn(),
708 | cancel: vi.fn(),
709 | }`
710 | );
711 | mock(
712 | "react-native/Libraries/Components/View/ViewNativeComponent",
713 | () => `(() => {
714 | const React = require("react");
715 | const Component = class extends React.Component {
716 | render() {
717 | return React.createElement("View", this.props, this.props.children);
718 | }
719 | };
720 |
721 | Component.displayName = "View";
722 |
723 | return {
724 | __esModule: true,
725 | default: Component,
726 | };
727 | })()`
728 | );
729 |
--------------------------------------------------------------------------------
/test/__snapshots__/Navigator.spec.jsx.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`navigator renders correctly 1`] = `
4 |
15 |
25 |
44 |
63 |
70 |
80 |
87 |
118 |
125 |
135 |
136 |
137 | Card 1
138 |
139 |
140 |
141 |
142 |
146 |
160 |
177 |
178 |
191 |
199 |
209 |
222 |
233 |
246 | Card1
247 |
248 |
249 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
273 |
287 |
304 |
305 |
318 |
326 |
336 |
349 |
360 |
373 | Home
374 |
375 |
376 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
420 |
432 |
441 |
495 |
505 |
523 |
537 | ⏷
538 |
539 |
540 |
558 |
572 | ⏷
573 |
574 |
575 |
576 |
594 | Home
595 |
596 |
597 |
647 |
657 |
675 |
689 | ⏷
690 |
691 |
692 |
710 |
724 | ⏷
725 |
726 |
727 |
728 |
746 | Profile
747 |
748 |
749 |
750 |
751 |
752 | `;
753 |
--------------------------------------------------------------------------------