Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/playground/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
projects: [
{
runner: '@react-native-harness/jest',
preset: '@react-native-harness/jest',
testMatch: [
'<rootDir>/src/__tests__/**/*.(test|spec|harness).(js|jsx|ts|tsx)',
],
Expand Down
19 changes: 19 additions & 0 deletions packages/jest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./jest-preset": {
"development": "./src/jest-preset.ts",
"types": "./dist/jest-preset.d.ts",
"import": "./dist/jest-preset.js",
"default": "./dist/jest-preset.js"
},
"./global-setup": {
"development": "./src/global-setup.ts",
"types": "./dist/global-setup.d.ts",
"import": "./dist/global-setup.js",
"default": "./dist/global-setup.js"
},
"./global-teardown": {
"development": "./src/global-teardown.ts",
"types": "./dist/global-teardown.d.ts",
"import": "./dist/global-teardown.js",
"default": "./dist/global-teardown.js"
}
},
"dependencies": {
Expand All @@ -22,6 +40,7 @@
"chalk": "^4.1.2",
"jest-message-util": "^30.2.0",
"jest-runner": "^30.2.0",
"jest-util": "^30.2.0",
"p-limit": "^7.1.1",
"tslib": "^2.3.0",
"yargs": "^17.7.2"
Expand Down
9 changes: 9 additions & 0 deletions packages/jest/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Harness } from '@react-native-harness/cli/external';
import type { Config as HarnessConfig } from '@react-native-harness/config';

declare global {
var HARNESS: Harness;
var HARNESS_CONFIG: HarnessConfig;
}

export {};
88 changes: 88 additions & 0 deletions packages/jest/src/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
getConfig,
TestRunnerConfig,
type Config as HarnessConfig,
type TestRunnerConfig as HarnessTestRunnerConfig,
} from '@react-native-harness/config';
import type { Config as JestConfig } from 'jest-runner';
import {
getHarness as getHarnessExternal,
type Harness,
} from '@react-native-harness/cli/external';
import { preRunMessage } from 'jest-util';
import { getAdditionalCliArgs, HarnessCliArgs } from './cli-args.js';
import { logTestEnvironmentReady, logTestRunHeader } from './logs.js';

const getHarnessConfig = async (
globalConfig: JestConfig.GlobalConfig
): Promise<HarnessConfig> => {
const projectRoot = globalConfig.rootDir;
const { config: harnessConfig } = await getConfig(projectRoot);
return harnessConfig;
};

const getHarnessRunner = (
config: HarnessConfig,
cliArgs: HarnessCliArgs
): HarnessTestRunnerConfig => {
const selectedRunnerName = cliArgs.harnessRunner ?? config.defaultRunner;
const runner = config.runners.find(
(runner) => runner.name === selectedRunnerName
);

if (!runner) {
throw new Error(`Runner "${selectedRunnerName}" not found`);
}

return runner;
};

const getHarness = async (runner: TestRunnerConfig): Promise<Harness> => {
return await getHarnessExternal(runner);
};

export default async function (globalConfig: JestConfig.GlobalConfig) {
preRunMessage.remove(process.stderr);
const harnessConfig =
global.HARNESS_CONFIG ?? (await getHarnessConfig(globalConfig));
const isWatchMode = globalConfig.watch || globalConfig.watchAll;

if (global.HARNESS) {
// Do not setup again if HARNESS is already initialized
// This is useful when running tests in watch mode

if (harnessConfig.resetEnvironmentBetweenTestFiles) {
// In watch mode, we want to restart the environment before each test run
await new Promise((resolve) => {
global.HARNESS.bridge.once('ready', resolve);
global.HARNESS.environment.restart();
});
}

return;
}

if (isWatchMode) {
// In watch mode, we want to dispose the Harness when the process exits.
process.on('exit', async () => {
await global.HARNESS.bridge.dispose();
await global.HARNESS.environment.dispose();
});
}

const cliArgs = getAdditionalCliArgs();
const selectedRunner = getHarnessRunner(harnessConfig, cliArgs);

if (globalConfig.collectCoverage) {
// This is going to be used by @react-native-harness/babel-preset
// to enable instrumentation of test files.
process.env.RN_HARNESS_COLLECT_COVERAGE = 'true';
}

logTestRunHeader(selectedRunner);
const harness = await getHarness(selectedRunner);
logTestEnvironmentReady(selectedRunner);

global.HARNESS_CONFIG = harnessConfig;
global.HARNESS = harness;
}
14 changes: 14 additions & 0 deletions packages/jest/src/global-teardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Config as JestConfig } from 'jest-runner';

export default async function (globalConfig: JestConfig.GlobalConfig) {
const isWatchMode = globalConfig.watch || globalConfig.watchAll;

if (isWatchMode) {
// In watch mode, we don't want to dispose the Harness.

return;
}

await global.HARNESS.bridge.dispose();
await global.HARNESS.environment.dispose();
}
67 changes: 13 additions & 54 deletions packages/jest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,8 @@ import type {
} from 'jest-runner';
import pLimit from 'p-limit';
import { runHarnessTestFile } from './run.js';
import { getHarness } from '@react-native-harness/cli/external';
import {
getConfig,
Config as HarnessConfig,
TestRunnerConfig as HarnessTestRunnerConfig,
} from '@react-native-harness/config';
import { getAdditionalCliArgs, HarnessCliArgs } from './cli-args.js';
import { Config as HarnessConfig } from '@react-native-harness/config';
import type { Harness } from '@react-native-harness/cli/external';
import { logTestEnvironmentReady, logTestRunHeader } from './logs.js';

class CancelRun extends Error {
constructor(message?: string) {
Expand All @@ -27,21 +20,6 @@ class CancelRun extends Error {
}
}

const getHarnessRunner = (
config: HarnessConfig,
cliArgs: HarnessCliArgs
): HarnessTestRunnerConfig => {
const selectedRunnerName = cliArgs.harnessRunner ?? config.defaultRunner;
const runner = config.runners.find(
(runner) => runner.name === selectedRunnerName
);

if (!runner) {
throw new Error(`Runner "${selectedRunnerName}" not found`);
}

return runner;
};
export default class JestHarness implements CallbackTestRunnerInterface {
readonly isSerial = true;

Expand All @@ -63,37 +41,18 @@ export default class JestHarness implements CallbackTestRunnerInterface {
throw new Error('Parallel test running is not supported');
}

const projectRoot = this.#globalConfig.rootDir;
const { config: harnessConfig } = await getConfig(projectRoot);
const cliArgs = getAdditionalCliArgs();
const selectedRunner = getHarnessRunner(harnessConfig, cliArgs);

logTestRunHeader(selectedRunner);

if (this.#globalConfig.collectCoverage) {
// This is going to be used by @react-native-harness/babel-preset
// to enable instrumentation of test files.
process.env.RN_HARNESS_COLLECT_COVERAGE = 'true';
}

const harness = await getHarness(selectedRunner);

logTestEnvironmentReady(selectedRunner);

try {
return await this._createInBandTestRun(
tests,
watcher,
harness,
harnessConfig,
onStart,
onResult,
onFailure
);
} finally {
harness.bridge.dispose();
await harness.environment.dispose();
}
const harness = global.HARNESS;
const harnessConfig = global.HARNESS_CONFIG;

return await this._createInBandTestRun(
tests,
watcher,
harness,
harnessConfig,
onStart,
onResult,
onFailure
);
}

async _createInBandTestRun(
Expand Down
5 changes: 5 additions & 0 deletions packages/jest/src/jest-preset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
runner: '@react-native-harness/jest',
globalSetup: '@react-native-harness/jest/global-setup',
globalTeardown: '@react-native-harness/jest/global-teardown',
};
104 changes: 9 additions & 95 deletions packages/runtime/assets/moduleSystem.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ global[`${__METRO_GLOBAL_PREFIX__}__d`] = (define: DefineFn);
global.__c = clear;
global.__registerSegment = registerSegment;
global.__resetAllModules = resetAllModules;
global.__clearModule = clearModule;

var modules = clear();

Expand All @@ -95,6 +96,14 @@ function resetAllModules() {
});
}

function clearModule(moduleId: ModuleID) {
if (!modules.has(moduleId)) {
return;
}

modules.delete(moduleId);
}

// Don't use a Symbol here, it would pull in an extra polyfill with all sorts of
// additional stuff (e.g. Array.from).
const EMPTY = {};
Expand Down Expand Up @@ -223,15 +232,6 @@ function metroRequire(

const module = modules.get(moduleIdReallyIsNumber);

// Optionally return a lazy proxy that triggers evaluation on first access.
if (
module &&
!module.isInitialized &&
shouldUseLazyModuleProxy(moduleIdReallyIsNumber, module)
) {
return createModuleEvaluationProxy(moduleIdReallyIsNumber);
}

return module && module.isInitialized
? module.publicModule.exports
: guardedLoadModule(moduleIdReallyIsNumber, module);
Expand Down Expand Up @@ -398,92 +398,6 @@ function packModuleId(value: {
}
metroRequire.packModuleId = packModuleId;

// ------------------------------
// Lazy evaluation via Proxy (opt-in)
// ------------------------------
const LAZY_REQUIRE_FLAG_KEY =
__METRO_GLOBAL_PREFIX__ + '__LAZY_REQUIRE_BY_ACCESS';
const LAZY_REQUIRE_ALLOW_DEV_KEY =
__METRO_GLOBAL_PREFIX__ + '__LAZY_REQUIRE_ALLOW_DEV';
const LAZY_REQUIRE_WHITELIST_KEY =
__METRO_GLOBAL_PREFIX__ + '__LAZY_REQUIRE_WHITELIST';

function isLazyRequireEnabled(): boolean {
// const enabled = global[LAZY_REQUIRE_FLAG_KEY];
// if (!enabled) {
// return false;
// }
// // In DEV, require explicit opt-in due to HMR/export inspection.
// return __DEV__ ? !!global[LAZY_REQUIRE_ALLOW_DEV_KEY] : true;
return true;
}

function isModuleWhitelisted(moduleId: ModuleID): boolean {
const list = global[LAZY_REQUIRE_WHITELIST_KEY];
if (list == null) {
// No whitelist provided -> treat as disabled for safety unless explicitly enabled
// at call site (we still require shouldUseLazyModuleProxy to decide).
return false;
}
return Array.isArray(list) && list.indexOf(moduleId) !== -1;
}

function shouldUseLazyModuleProxy(
moduleId: ModuleID,
module: ?ModuleDefinition
): boolean {
if (!isLazyRequireEnabled()) {
return false;
}
if (!module || module.isInitialized) {
return false;
}
// Only enable for explicitly whitelisted modules to avoid changing semantics
// of side-effect-only requires.
return isModuleWhitelisted(moduleId);
}

function createModuleEvaluationProxy(moduleId: ModuleID): Exports {
let evaluated = false;
const ensure = (): Exports => {
if (!evaluated) {
// Evaluate the module using the same guarded path used by metroRequire.
// Pass the current (possibly undefined) ModuleDefinition for better error messages.
const existing = modules.get(moduleId);
// guardedLoadModule will throw appropriately if unknown or failing.
// It will also set module.isInitialized and publicModule.exports.
// $FlowFixMe[incompatible-call]
guardedLoadModule(moduleId, existing);
console.log('evaluated module', moduleId);
evaluated = true;
}
// $FlowFixMe[incompatible-type]
const initializedModule: ModuleDefinition | void = modules.get(moduleId);
// $FlowFixMe[incompatible-use]
return initializedModule
? initializedModule.publicModule.exports
: undefined;
};

// Use an object target since CommonJS exports are objects in Metro.
const target = {};
return new Proxy(target, {
get: (_t, prop) => Reflect.get(ensure(), prop),
set: (_t, prop, value) => Reflect.set(ensure(), prop, value),
has: (_t, prop) => Reflect.has(ensure(), prop),
ownKeys: () => Reflect.ownKeys(ensure()),
getOwnPropertyDescriptor: (_t, prop) =>
Reflect.getOwnPropertyDescriptor(ensure(), prop),
getPrototypeOf: () => Reflect.getPrototypeOf(ensure()),
setPrototypeOf: (_t, proto) => Reflect.setPrototypeOf(ensure(), proto),
isExtensible: () => Reflect.isExtensible(ensure()),
preventExtensions: () => Reflect.preventExtensions(ensure()),
defineProperty: (_t, prop, desc) =>
Reflect.defineProperty(ensure(), prop, desc),
deleteProperty: (_t, prop) => Reflect.deleteProperty(ensure(), prop),
});
}

const moduleDefinersBySegmentID: Array<?ModuleDefiner> = [];
const definingSegmentByModuleID: Map<ModuleID, number> = new Map();

Expand Down
Loading