jest-expo 58.0.6 → 58.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,11 +5,15 @@ const { withWatchPlugins } = require('./withWatchPlugins');
5
5
  const expoPreset = require('../jest-preset');
6
6
  const { resolveBabelOptions } = require('../src/resolveBabelOptions');
7
7
 
8
+ const babelJestPath = require.resolve('babel-jest');
9
+
10
+ function isBabelJest(transformer) {
11
+ return transformer === 'babel-jest' || transformer === babelJestPath;
12
+ }
13
+
8
14
  function getUpstreamBabelJest(transform) {
9
15
  const upstreamBabelJest = Object.keys(transform).find((key) =>
10
- Array.isArray(transform[key])
11
- ? transform[key][0] === 'babel-jest'
12
- : transform[key] === 'babel-jest'
16
+ Array.isArray(transform[key]) ? isBabelJest(transform[key][0]) : isBabelJest(transform[key])
13
17
  );
14
18
  return upstreamBabelJest;
15
19
  }
@@ -51,7 +55,7 @@ function getPlatformPreset(displayOptions, extensions, platform, { isServer, isR
51
55
  transform: {
52
56
  ...expoPreset.transform,
53
57
  [upstreamBabelJest]: [
54
- 'babel-jest',
58
+ babelJestPath,
55
59
  {
56
60
  ...babelJestOptions,
57
61
  caller: {
@@ -103,7 +107,10 @@ function getPlatformPreset(displayOptions, extensions, platform, { isServer, isR
103
107
  // Source exports can contain TypeScript files that use explicit `.js`
104
108
  // extensions for runtime ESM compatibility.
105
109
  '^(\\.{1,2}/.*)\\.js$': '$1',
106
- '^react-native/asset-registry$': 'react-native/src/asset-registry',
110
+ // See the note in `../jest-preset.js`: mapped targets must be absolute paths on Jest 30.
111
+ '^react-native/asset-registry$': expoPreset.moduleNameMapper['^react-native/asset-registry$'],
112
+ '^react-native/unstable-internals-do-not-use$':
113
+ expoPreset.moduleNameMapper['^react-native/unstable-internals-do-not-use$'],
107
114
  ...preset.moduleNameMapper,
108
115
  };
109
116
 
@@ -151,16 +158,17 @@ function getBaseWebPreset() {
151
158
  }
152
159
 
153
160
  // `getPlatformPreset` builds its own `moduleNameMapper` from scratch, so spreading it over the
154
- // base preset drops the mappers that the upstream React Native preset defines. Since React Native
155
- // 0.87 those mappers are required: `jest/setup.js` mocks `react-native/setup-env`, which is only
156
- // reachable through the `^react-native/setup-env$` alias. Merge them back for the native presets,
157
- // keeping the platform mappers last so platform-specific entries still win.
158
- function withNativeModuleNameMapper(platformPreset) {
161
+ // base preset drops the mappers that the base preset defines. Those mappers are required: since
162
+ // React Native 0.87 `jest/setup.js` mocks `react-native/setup-env`, which is only reachable
163
+ // through the `^react-native/setup-env$` alias, and `withTypescriptMapping` adds the aliases from
164
+ // the project's `tsconfig.json` `paths`. Merge them back for every platform preset, keeping the
165
+ // platform mappers last so platform-specific entries still win.
166
+ function withBaseModuleNameMapper(basePreset, platformPreset) {
159
167
  return {
160
- ...expoPreset,
168
+ ...basePreset,
161
169
  ...platformPreset,
162
170
  moduleNameMapper: {
163
- ...expoPreset.moduleNameMapper,
171
+ ...basePreset.moduleNameMapper,
164
172
  ...platformPreset.moduleNameMapper,
165
173
  },
166
174
  };
@@ -168,32 +176,32 @@ function withNativeModuleNameMapper(platformPreset) {
168
176
 
169
177
  module.exports = {
170
178
  getWebPreset({ isReactServer } = {}) {
171
- const preset = {
172
- ...getBaseWebPreset(),
173
- testEnvironment: 'jsdom',
174
- ...getPlatformPreset({ name: 'Web', color: 'magenta' }, ['web'], 'web', {
179
+ return withBaseModuleNameMapper(
180
+ { ...getBaseWebPreset(), testEnvironment: 'jsdom' },
181
+ getPlatformPreset({ name: 'Web', color: 'magenta' }, ['web'], 'web', {
175
182
  isReactServer,
176
- }),
177
- };
178
- return preset;
183
+ })
184
+ );
179
185
  },
180
186
  getNodePreset() {
181
- return {
182
- ...getBaseWebPreset(),
183
- ...getPlatformPreset({ name: 'Node', color: 'cyan' }, ['node', 'web'], 'web', {
187
+ return withBaseModuleNameMapper(
188
+ getBaseWebPreset(),
189
+ getPlatformPreset({ name: 'Node', color: 'cyan' }, ['node', 'web'], 'web', {
184
190
  isServer: true,
185
- }),
186
- };
191
+ })
192
+ );
187
193
  },
188
194
  getIOSPreset({ isReactServer } = {}) {
189
- return withNativeModuleNameMapper(
195
+ return withBaseModuleNameMapper(
196
+ expoPreset,
190
197
  getPlatformPreset({ name: 'iOS', color: 'white' }, ['ios', 'native'], 'ios', {
191
198
  isReactServer,
192
199
  })
193
200
  );
194
201
  },
195
202
  getAndroidPreset({ isReactServer } = {}) {
196
- return withNativeModuleNameMapper(
203
+ return withBaseModuleNameMapper(
204
+ expoPreset,
197
205
  getPlatformPreset(
198
206
  { name: 'Android', color: 'blueBright' },
199
207
  ['android', 'native'],
@@ -24,9 +24,13 @@ function withWatchPlugins({ watchPlugins = [], ...config }) {
24
24
  if (Array.isArray(config.projects)) {
25
25
  let projectsWantPass = false;
26
26
  config.projects = config.projects.map((project) => {
27
- if (project && typeof project === 'object' && 'passWithNoTests' in project) {
28
- const { passWithNoTests, ...rest } = project;
29
- projectsWantPass ||= passWithNoTests;
27
+ if (project && typeof project === 'object') {
28
+ // `watchPlugins` and `passWithNoTests` are root-only options in multi-project mode.
29
+ // Strip them from each project to avoid Jest's validation warnings.
30
+ const { watchPlugins, passWithNoTests, ...rest } = project;
31
+ if ('passWithNoTests' in project) {
32
+ projectsWantPass ||= passWithNoTests;
33
+ }
30
34
  return rest;
31
35
  }
32
36
  return project;
package/jest-preset.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const cloneDeep = require('lodash/cloneDeep');
4
4
  const isEqual = require('lodash/isEqual');
5
+ const path = require('node:path');
5
6
 
6
7
  let jestPreset;
7
8
  try {
@@ -23,12 +24,37 @@ try {
23
24
 
24
25
  jestPreset = cloneDeep(jestPreset);
25
26
 
27
+ // Use jest-expo's own copy of the React Native test environment. The upstream preset resolves
28
+ // `jest-environment-node` from its own (Jest 29) dependencies, which must not be mixed into a Jest 30
29
+ // runtime. The environment is otherwise identical to `@react-native/jest-preset/jest/react-native-env`.
30
+ jestPreset.testEnvironment = require.resolve('./src/preset/nativeEnvironment.js');
31
+
26
32
  const { withTypescriptMapping } = require('./src/preset/withTypescriptMapping');
27
33
  const { resolveBabelOptions } = require('./src/resolveBabelOptions');
28
34
 
35
+ // NOTE: Jest 30 replaced its resolver with `unrs-resolver` and dropped the `packageFilter` option,
36
+ // so `@react-native/jest-preset`'s resolver can no longer strip `react-native`'s `exports` map.
37
+ // Mapped module targets must be absolute paths, since bare `react-native/src/*` subpaths are
38
+ // not listed in `exports`.
39
+ const reactNativeAssetRegistry = path.join(
40
+ path.dirname(require.resolve('react-native/package.json')),
41
+ 'src/asset-registry.js'
42
+ );
43
+ // Resolve from the upstream preset so the entry point's relative requires load the same
44
+ // `react-native` copy its mocks are registered against.
45
+ const reactNativeUnstableInternals = path.join(
46
+ path.dirname(
47
+ require.resolve('react-native/package.json', {
48
+ paths: [path.dirname(require.resolve('@react-native/jest-preset/package.json'))],
49
+ })
50
+ ),
51
+ 'src/unstable-internals-do-not-use.js'
52
+ );
53
+
29
54
  // Emulate the alias behavior of Expo's Metro resolver.
30
55
  jestPreset.moduleNameMapper = {
31
- '^react-native/asset-registry$': 'react-native/src/asset-registry',
56
+ '^react-native/asset-registry$': reactNativeAssetRegistry,
57
+ '^react-native/unstable-internals-do-not-use$': reactNativeUnstableInternals,
32
58
  ...(jestPreset.moduleNameMapper || {}),
33
59
  '^react-native-vector-icons$': '@expo/vector-icons',
34
60
  '^react-native-vector-icons/(.*)': '@expo/vector-icons/$1',
@@ -42,8 +68,12 @@ if (upstreamBabelJest) {
42
68
  }
43
69
 
44
70
  // transform
71
+ // Resolve `babel-jest` from jest-expo so the Jest 30 version is used. `@react-native/jest-preset`
72
+ // still depends on `babel-jest@29`, and a bare `'babel-jest'` would resolve from the project root,
73
+ // where hoisting decides which of the two copies wins.
74
+ const babelJestPath = require.resolve('babel-jest');
45
75
  const babelOpts = resolveBabelOptions(process.cwd());
46
- jestPreset.transform['\\.[jt]sx?$'] = ['babel-jest', babelOpts];
76
+ jestPreset.transform['\\.[jt]sx?$'] = [babelJestPath, babelOpts];
47
77
 
48
78
  /* Update this when metro changes their default extensions */
49
79
  const defaultMetroAssetExts = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jest-expo",
3
- "version": "58.0.6",
3
+ "version": "58.0.8",
4
4
  "description": "A Jest preset to painlessly test your Expo / React Native apps.",
5
5
  "homepage": "https://github.com/expo/expo/tree/main/packages/jest-expo",
6
6
  "bugs": {
@@ -30,27 +30,31 @@
30
30
  "main": "src/index.js",
31
31
  "types": "src/index.d.ts",
32
32
  "dependencies": {
33
- "@jest/create-cache-key-function": "^29.2.1",
34
- "@jest/globals": "^29.2.1",
35
- "babel-jest": "^29.2.1",
36
- "jest-environment-jsdom": "^29.2.1",
37
- "jest-snapshot": "^29.2.1",
33
+ "@jest/create-cache-key-function": "^30.0.0",
34
+ "@jest/globals": "^30.0.0",
35
+ "babel-jest": "^30.0.0",
36
+ "jest-environment-jsdom": "^30.0.0",
37
+ "jest-environment-node": "^30.0.0",
38
+ "jest-mock": "^30.0.0",
39
+ "jest-snapshot": "^30.0.0",
38
40
  "jest-watch-select-projects": "^2.0.0",
39
- "jest-watch-typeahead": "2.2.1",
41
+ "jest-watch-typeahead": "^3.0.1",
40
42
  "json5": "^2.2.3",
41
- "lodash": "^4.17.19",
43
+ "lodash": "^4.18.1",
42
44
  "server-only": "^0.0.1",
43
45
  "stacktrace-js": "^2.0.2",
44
46
  "test-renderer": "1.3.0"
45
47
  },
46
48
  "devDependencies": {
47
49
  "@react-native/jest-preset": "0.88.0-rc.3",
50
+ "@types/node": "^22.14.0",
48
51
  "@types/react": "~19.3.0",
49
52
  "react-server-dom-webpack": "~19.0.8"
50
53
  },
51
54
  "peerDependencies": {
52
55
  "@react-native/jest-preset": "^0.88.0-rc.3",
53
56
  "expo": "*",
57
+ "jest": "^30.0.0",
54
58
  "react-native": "*",
55
59
  "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4 || ~19.3.0"
56
60
  },
@@ -10,8 +10,5 @@ module.exports = withWatchPlugins({
10
10
  getIOSPreset({ isReactServer: true }),
11
11
  getAndroidPreset({ isReactServer: true }),
12
12
  getWebPreset({ isReactServer: true }),
13
- // Remove sub-watch-plugins from the preset when using multi-project runner.
14
- ].map(({ watchPlugins, ...config }) => config),
15
- // See: https://jestjs.io/docs/configuration#prettierpath-string
16
- prettierPath: require.resolve('jest-snapshot-prettier'),
13
+ ],
17
14
  });
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const { TestEnvironment: NodeEnvironment } = require('jest-environment-node');
4
+
5
+ /**
6
+ * Clear mocks exposed on the global scope, but only look at data properties.
7
+ *
8
+ * Since Jest 30.4, `Runtime.resetModules()` (also run during teardown) calls
9
+ * `moduleMocker.clearMocksOnScope(global)`, which reads every enumerable global. Expo installs
10
+ * several globals lazily (`fetch`, `structuredClone`, `__ExpoImportMetaRegistry`, ...) through
11
+ * getters that `require()` their implementation on first access. Reading them after the test has
12
+ * finished throws `You are trying to require a file outside of the scope of the test code`.
13
+ * Accessor properties are skipped here so lazy globals are never evaluated during teardown.
14
+ *
15
+ * @this {import('jest-mock').ModuleMocker}
16
+ * @param {object} scope
17
+ */
18
+ function clearMocksOnScope(scope) {
19
+ for (const key of Object.keys(scope)) {
20
+ const descriptor = Object.getOwnPropertyDescriptor(scope, key);
21
+ if (!descriptor || typeof descriptor.get === 'function') {
22
+ continue;
23
+ }
24
+ const value = descriptor.value;
25
+ if (
26
+ value != null &&
27
+ (typeof value === 'object' || typeof value === 'function') &&
28
+ '_isMockFunction' in value &&
29
+ this.isMockFunction(value) &&
30
+ typeof value.mockClear === 'function'
31
+ ) {
32
+ value.mockClear();
33
+ }
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Jest test environment for the native (iOS/Android) presets.
39
+ *
40
+ * Mirrors `@react-native/jest-preset/jest/react-native-env.js`, but resolves `jest-environment-node`
41
+ * from `jest-expo` instead of from `@react-native/jest-preset`. The React Native preset still pins
42
+ * Jest 29 packages, so on Jest 30 its environment would load `jest-environment-node@29`, mixing
43
+ * Jest 29 mocks and fake timers into a Jest 30 runtime (see `clearMocksOnScope is not a function`).
44
+ */
45
+ class ReactNativeEnvironment extends NodeEnvironment {
46
+ customExportConditions = ['require', 'react-native'];
47
+
48
+ constructor(config, context) {
49
+ super(config, context);
50
+ // Patch the module mocker in place. `NodeEnvironment` already handed this instance to the
51
+ // legacy fake timers, so replacing it would leave timer mocks owned by a mocker the runtime
52
+ // never sees (and `jest.clearAllMocks()` would no longer clear them).
53
+ this.moduleMocker.clearMocksOnScope = clearMocksOnScope;
54
+ }
55
+ }
56
+
57
+ module.exports = ReactNativeEnvironment;
58
+ module.exports.TestEnvironment = ReactNativeEnvironment;