@react-native-harness/jest 1.0.0-canary.1766225407244 → 1.0.0
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.
- package/dist/__tests__/client-log-handler.test.d.ts +2 -0
- package/dist/__tests__/client-log-handler.test.d.ts.map +1 -0
- package/dist/__tests__/client-log-handler.test.js +262 -0
- package/dist/cli-args.test.d.ts +2 -0
- package/dist/cli-args.test.d.ts.map +1 -0
- package/dist/cli-args.test.js +1 -0
- package/dist/client-log-handler.d.ts +23 -0
- package/dist/client-log-handler.d.ts.map +1 -0
- package/dist/client-log-handler.js +84 -0
- package/dist/crash-monitor.d.ts +16 -0
- package/dist/crash-monitor.d.ts.map +1 -0
- package/dist/crash-monitor.js +75 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +17 -0
- package/dist/event-mapper.d.ts +16 -0
- package/dist/event-mapper.d.ts.map +1 -0
- package/dist/event-mapper.js +120 -0
- package/dist/global-setup.d.ts +3 -0
- package/dist/global-setup.d.ts.map +1 -0
- package/dist/global-setup.js +60 -0
- package/dist/global-teardown.d.ts +3 -0
- package/dist/global-teardown.d.ts.map +1 -0
- package/dist/global-teardown.js +11 -0
- package/dist/harness-reporter.d.ts +8 -0
- package/dist/harness-reporter.d.ts.map +1 -0
- package/dist/harness-reporter.js +12 -0
- package/dist/harness.d.ts +17 -3
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +114 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -8
- package/dist/jest-options.d.ts +2 -0
- package/dist/jest-options.d.ts.map +1 -0
- package/dist/jest-options.js +1 -0
- package/dist/logs.d.ts +1 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +3 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +1 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +6 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/eslint.config.mjs +5 -2
- package/package.json +6 -7
- package/src/__tests__/client-log-handler.test.ts +328 -0
- package/src/client-log-handler.ts +101 -0
- package/src/crash-monitor.ts +109 -0
- package/src/errors.ts +20 -0
- package/src/harness.ts +173 -29
- package/src/index.ts +46 -10
- package/src/logs.ts +6 -0
- package/src/run.ts +1 -0
- package/src/setup.ts +8 -0
- package/tsconfig.json +3 -3
- package/vite.config.ts +18 -0
package/src/harness.ts
CHANGED
|
@@ -1,17 +1,128 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getBridgeServer,
|
|
3
|
+
BridgeServer,
|
|
4
|
+
} from '@react-native-harness/bridge/server';
|
|
5
|
+
import {
|
|
6
|
+
HarnessContext,
|
|
7
|
+
TestExecutionOptions,
|
|
8
|
+
TestSuiteResult,
|
|
9
|
+
} from '@react-native-harness/bridge';
|
|
10
|
+
import {
|
|
11
|
+
HarnessPlatform,
|
|
12
|
+
HarnessPlatformRunner,
|
|
13
|
+
} from '@react-native-harness/platforms';
|
|
14
|
+
import {
|
|
15
|
+
getMetroInstance,
|
|
16
|
+
prewarmMetroBundle,
|
|
17
|
+
Reporter,
|
|
18
|
+
ReportableEvent,
|
|
19
|
+
} from '@react-native-harness/bundler-metro';
|
|
20
|
+
import { InitializationTimeoutError, MaxAppRestartsError } from './errors.js';
|
|
6
21
|
import { Config as HarnessConfig } from '@react-native-harness/config';
|
|
7
|
-
import
|
|
22
|
+
import { createCrashMonitor, CrashMonitor } from './crash-monitor.js';
|
|
23
|
+
import { createClientLogListener } from './client-log-handler.js';
|
|
24
|
+
import { logMetroPrewarmCompleted } from './logs.js';
|
|
8
25
|
|
|
9
|
-
|
|
26
|
+
export type HarnessRunTestsOptions = Exclude<TestExecutionOptions, 'platform'>;
|
|
10
27
|
|
|
11
28
|
export type Harness = {
|
|
12
|
-
|
|
29
|
+
context: HarnessContext;
|
|
30
|
+
runTests: (
|
|
31
|
+
path: string,
|
|
32
|
+
options: HarnessRunTestsOptions
|
|
33
|
+
) => Promise<TestSuiteResult>;
|
|
13
34
|
restart: () => Promise<void>;
|
|
14
35
|
dispose: () => Promise<void>;
|
|
36
|
+
crashMonitor: CrashMonitor;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const waitForAppReady = async (options: {
|
|
40
|
+
metroEvents: Reporter;
|
|
41
|
+
serverBridge: BridgeServer;
|
|
42
|
+
platformInstance: HarnessPlatformRunner;
|
|
43
|
+
bundleStartTimeout: number;
|
|
44
|
+
maxRestarts: number;
|
|
45
|
+
signal: AbortSignal;
|
|
46
|
+
}): Promise<void> => {
|
|
47
|
+
const {
|
|
48
|
+
metroEvents,
|
|
49
|
+
serverBridge,
|
|
50
|
+
platformInstance,
|
|
51
|
+
bundleStartTimeout,
|
|
52
|
+
maxRestarts,
|
|
53
|
+
signal,
|
|
54
|
+
} = options;
|
|
55
|
+
|
|
56
|
+
let restartCount = 0;
|
|
57
|
+
let isBundling = false;
|
|
58
|
+
let bundleTimeoutId: NodeJS.Timeout | null = null;
|
|
59
|
+
|
|
60
|
+
const clearBundleTimeout = () => {
|
|
61
|
+
if (bundleTimeoutId) {
|
|
62
|
+
clearTimeout(bundleTimeoutId);
|
|
63
|
+
bundleTimeoutId = null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return new Promise<void>((resolve, reject) => {
|
|
68
|
+
// Handle abort signal
|
|
69
|
+
signal.addEventListener('abort', () => {
|
|
70
|
+
clearBundleTimeout();
|
|
71
|
+
reject(new DOMException('The operation was aborted', 'AbortError'));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Start/restart the bundle timeout
|
|
75
|
+
const startBundleTimeout = () => {
|
|
76
|
+
clearBundleTimeout();
|
|
77
|
+
bundleTimeoutId = setTimeout(() => {
|
|
78
|
+
if (isBundling) return; // Don't restart while bundling
|
|
79
|
+
|
|
80
|
+
if (restartCount >= maxRestarts) {
|
|
81
|
+
cleanup();
|
|
82
|
+
reject(new MaxAppRestartsError(restartCount + 1));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
restartCount++;
|
|
87
|
+
platformInstance.restartApp().catch(reject);
|
|
88
|
+
startBundleTimeout(); // Reset timer for next attempt
|
|
89
|
+
}, bundleStartTimeout);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Metro event listener
|
|
93
|
+
const onMetroEvent = (event: ReportableEvent) => {
|
|
94
|
+
if (event.type === 'bundle_build_started') {
|
|
95
|
+
isBundling = true;
|
|
96
|
+
clearBundleTimeout(); // Cancel restart timer while bundling
|
|
97
|
+
} else if (
|
|
98
|
+
event.type === 'bundle_build_done' ||
|
|
99
|
+
event.type === 'bundle_build_failed'
|
|
100
|
+
) {
|
|
101
|
+
isBundling = false;
|
|
102
|
+
startBundleTimeout(); // Reset timer after bundle completes
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Bridge ready listener
|
|
107
|
+
const onReady = () => {
|
|
108
|
+
cleanup();
|
|
109
|
+
resolve();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const cleanup = () => {
|
|
113
|
+
clearBundleTimeout();
|
|
114
|
+
metroEvents.removeListener(onMetroEvent);
|
|
115
|
+
serverBridge.off('ready', onReady);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Setup listeners
|
|
119
|
+
metroEvents.addListener(onMetroEvent);
|
|
120
|
+
serverBridge.once('ready', onReady);
|
|
121
|
+
|
|
122
|
+
// Start the app and timeout
|
|
123
|
+
platformInstance.restartApp().catch(reject);
|
|
124
|
+
startBundleTimeout();
|
|
125
|
+
});
|
|
15
126
|
};
|
|
16
127
|
|
|
17
128
|
const getHarnessInternal = async (
|
|
@@ -20,16 +131,33 @@ const getHarnessInternal = async (
|
|
|
20
131
|
projectRoot: string,
|
|
21
132
|
signal: AbortSignal
|
|
22
133
|
): Promise<Harness> => {
|
|
134
|
+
const context: HarnessContext = {
|
|
135
|
+
platform,
|
|
136
|
+
};
|
|
137
|
+
|
|
23
138
|
const [metroInstance, platformInstance, serverBridge] = await Promise.all([
|
|
24
139
|
getMetroInstance({ projectRoot, harnessConfig: config }, signal),
|
|
25
|
-
import(platform.runner).then((module) =>
|
|
140
|
+
import(platform.runner).then((module) =>
|
|
141
|
+
module.default(platform.config, config)
|
|
142
|
+
),
|
|
26
143
|
getBridgeServer({
|
|
27
|
-
port:
|
|
144
|
+
port: config.webSocketPort,
|
|
28
145
|
timeout: config.bridgeTimeout,
|
|
146
|
+
context,
|
|
29
147
|
}),
|
|
30
148
|
]);
|
|
31
149
|
|
|
150
|
+
// Forward client logs to console if enabled
|
|
151
|
+
const clientLogListener = createClientLogListener();
|
|
152
|
+
|
|
153
|
+
if (config.forwardClientLogs) {
|
|
154
|
+
metroInstance.events.addListener(clientLogListener);
|
|
155
|
+
}
|
|
156
|
+
|
|
32
157
|
const dispose = async () => {
|
|
158
|
+
if (config.forwardClientLogs) {
|
|
159
|
+
metroInstance.events.removeListener(clientLogListener);
|
|
160
|
+
}
|
|
33
161
|
await Promise.all([
|
|
34
162
|
serverBridge.dispose(),
|
|
35
163
|
platformInstance.dispose(),
|
|
@@ -37,6 +165,12 @@ const getHarnessInternal = async (
|
|
|
37
165
|
]);
|
|
38
166
|
};
|
|
39
167
|
|
|
168
|
+
const crashMonitor = createCrashMonitor({
|
|
169
|
+
interval: config.crashDetectionInterval,
|
|
170
|
+
platformRunner: platformInstance,
|
|
171
|
+
bridgeServer: serverBridge,
|
|
172
|
+
});
|
|
173
|
+
|
|
40
174
|
if (signal.aborted) {
|
|
41
175
|
await dispose();
|
|
42
176
|
|
|
@@ -44,23 +178,24 @@ const getHarnessInternal = async (
|
|
|
44
178
|
}
|
|
45
179
|
|
|
46
180
|
try {
|
|
47
|
-
await
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
181
|
+
await prewarmMetroBundle({
|
|
182
|
+
projectRoot,
|
|
183
|
+
entryPoint: config.entryPoint,
|
|
184
|
+
platform: platform.platformId,
|
|
185
|
+
dev: true,
|
|
186
|
+
minify: false,
|
|
187
|
+
signal,
|
|
188
|
+
});
|
|
189
|
+
logMetroPrewarmCompleted(platform);
|
|
190
|
+
|
|
191
|
+
await waitForAppReady({
|
|
192
|
+
metroEvents: metroInstance.events,
|
|
193
|
+
serverBridge,
|
|
194
|
+
platformInstance: platformInstance as HarnessPlatformRunner,
|
|
195
|
+
bundleStartTimeout: config.bundleStartTimeout,
|
|
196
|
+
maxRestarts: config.maxAppRestarts,
|
|
197
|
+
signal,
|
|
198
|
+
});
|
|
64
199
|
} catch (error) {
|
|
65
200
|
await dispose();
|
|
66
201
|
throw error;
|
|
@@ -68,11 +203,16 @@ const getHarnessInternal = async (
|
|
|
68
203
|
|
|
69
204
|
const restart = () =>
|
|
70
205
|
new Promise<void>((resolve, reject) => {
|
|
71
|
-
|
|
206
|
+
crashMonitor.markIntentionalRestart();
|
|
207
|
+
serverBridge.once('ready', () => {
|
|
208
|
+
crashMonitor.clearIntentionalRestart();
|
|
209
|
+
resolve();
|
|
210
|
+
});
|
|
72
211
|
platformInstance.restartApp().catch(reject);
|
|
73
212
|
});
|
|
74
213
|
|
|
75
214
|
return {
|
|
215
|
+
context,
|
|
76
216
|
runTests: async (path, options) => {
|
|
77
217
|
const client = serverBridge.rpc.clients.at(-1);
|
|
78
218
|
|
|
@@ -80,10 +220,14 @@ const getHarnessInternal = async (
|
|
|
80
220
|
throw new Error('No client found');
|
|
81
221
|
}
|
|
82
222
|
|
|
83
|
-
return await client.runTests(path,
|
|
223
|
+
return await client.runTests(path, {
|
|
224
|
+
...options,
|
|
225
|
+
runner: platform.runner,
|
|
226
|
+
});
|
|
84
227
|
},
|
|
85
228
|
restart,
|
|
86
229
|
dispose,
|
|
230
|
+
crashMonitor,
|
|
87
231
|
};
|
|
88
232
|
};
|
|
89
233
|
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,8 @@ import { setup } from './setup.js';
|
|
|
16
16
|
import { teardown } from './teardown.js';
|
|
17
17
|
import { HarnessError } from '@react-native-harness/tools';
|
|
18
18
|
import { getErrorMessage } from './logs.js';
|
|
19
|
-
import { DeviceNotRespondingError } from '@react-native-harness/bridge';
|
|
19
|
+
import { DeviceNotRespondingError } from '@react-native-harness/bridge/server';
|
|
20
|
+
import { NativeCrashError } from './errors.js';
|
|
20
21
|
|
|
21
22
|
class CancelRun extends Error {
|
|
22
23
|
constructor(message?: string) {
|
|
@@ -104,17 +105,52 @@ export default class JestHarness implements CallbackTestRunnerInterface {
|
|
|
104
105
|
}
|
|
105
106
|
isFirstTest = false;
|
|
106
107
|
|
|
107
|
-
return onStart(test).then(() =>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
return onStart(test).then(async () => {
|
|
109
|
+
if (!harnessConfig.detectNativeCrashes) {
|
|
110
|
+
return runHarnessTestFile({
|
|
111
|
+
testPath: test.path,
|
|
112
|
+
harness,
|
|
113
|
+
globalConfig: this.#globalConfig,
|
|
114
|
+
projectConfig: test.context.config,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Start crash monitoring
|
|
119
|
+
const crashPromise = harness.crashMonitor.startMonitoring(
|
|
120
|
+
test.path
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const result = await Promise.race([
|
|
125
|
+
runHarnessTestFile({
|
|
126
|
+
testPath: test.path,
|
|
127
|
+
harness,
|
|
128
|
+
globalConfig: this.#globalConfig,
|
|
129
|
+
projectConfig: test.context.config,
|
|
130
|
+
}),
|
|
131
|
+
crashPromise,
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
return result;
|
|
135
|
+
} finally {
|
|
136
|
+
harness.crashMonitor.stopMonitoring();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
115
139
|
})
|
|
116
140
|
.then((result) => onResult(test, result))
|
|
117
|
-
.catch((err) => {
|
|
141
|
+
.catch(async (err) => {
|
|
142
|
+
if (err instanceof NativeCrashError) {
|
|
143
|
+
onFailure(test, {
|
|
144
|
+
message: err.message,
|
|
145
|
+
stack: '',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Restart the app for the next test file
|
|
149
|
+
await harness.restart();
|
|
150
|
+
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
118
154
|
if (err instanceof DeviceNotRespondingError) {
|
|
119
155
|
onFailure(test, {
|
|
120
156
|
message: err.message,
|
package/src/logs.ts
CHANGED
|
@@ -25,6 +25,12 @@ export const logTestEnvironmentReady = (runner: HarnessPlatform): void => {
|
|
|
25
25
|
log(`${TAG} Runner ${chalk.bold(runner.name)} ready\n`);
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
export const logMetroPrewarmCompleted = (runner: HarnessPlatform): void => {
|
|
29
|
+
log(
|
|
30
|
+
`${TAG} Metro pre-warm for ${chalk.bold(runner.name)} completed\n`
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
28
34
|
export const getErrorMessage = (error: HarnessError): string => {
|
|
29
35
|
return `${ERROR_TAG} ${error.message}\n`;
|
|
30
36
|
};
|
package/src/run.ts
CHANGED
package/src/setup.ts
CHANGED
|
@@ -68,6 +68,14 @@ export const setup = async (globalConfig: JestConfig.GlobalConfig) => {
|
|
|
68
68
|
// This is going to be used by @react-native-harness/babel-preset
|
|
69
69
|
// to enable instrumentation of test files.
|
|
70
70
|
process.env.RN_HARNESS_COLLECT_COVERAGE = 'true';
|
|
71
|
+
|
|
72
|
+
if (harnessConfig.coverage?.root) {
|
|
73
|
+
process.env.RN_HARNESS_COVERAGE_ROOT = harnessConfig.coverage.root;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (harnessConfig.disableViewFlattening) {
|
|
78
|
+
process.env.RN_HARNESS_VIEW_FLATTENING = 'false';
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
logTestRunHeader(selectedRunner);
|
package/tsconfig.json
CHANGED
package/vite.config.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types='vitest' />
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
|
|
4
|
+
export default defineConfig(() => ({
|
|
5
|
+
root: __dirname,
|
|
6
|
+
cacheDir: '../../node_modules/.vite/packages/jest',
|
|
7
|
+
test: {
|
|
8
|
+
watch: false,
|
|
9
|
+
globals: true,
|
|
10
|
+
environment: 'node',
|
|
11
|
+
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
12
|
+
reporters: ['default'],
|
|
13
|
+
coverage: {
|
|
14
|
+
reportsDirectory: './test-output/vitest/coverage',
|
|
15
|
+
provider: 'v8' as const,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
}));
|