├── .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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /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 | 752 | `; 753 | --------------------------------------------------------------------------------