@react-native-harness/jest 1.2.0 → 1.4.0-rc.1
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__/bridge.test.js +23 -2
- package/dist/__tests__/crash-monitor.test.js +146 -192
- package/dist/__tests__/errors.test.js +39 -0
- package/dist/__tests__/execute-run.test.js +190 -26
- package/dist/__tests__/fixtures/mock-platform-runner.d.ts +7 -0
- package/dist/__tests__/fixtures/mock-platform-runner.d.ts.map +1 -0
- package/dist/__tests__/fixtures/mock-platform-runner.js +6 -0
- package/dist/__tests__/harness-session.test.d.ts +2 -0
- package/dist/__tests__/harness-session.test.d.ts.map +1 -0
- package/dist/__tests__/harness-session.test.js +74 -0
- package/dist/__tests__/index.test.js +59 -10
- package/dist/__tests__/test-file-platform-filter.test.d.ts +2 -0
- package/dist/__tests__/test-file-platform-filter.test.d.ts.map +1 -0
- package/dist/__tests__/test-file-platform-filter.test.js +47 -0
- package/dist/crash-monitor.d.ts +5 -4
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +116 -102
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +41 -2
- package/dist/execute-run.d.ts +4 -2
- package/dist/execute-run.d.ts.map +1 -1
- package/dist/execute-run.js +85 -7
- package/dist/harness-session.d.ts +21 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +134 -39
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -2
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +1 -0
- package/dist/test-file-platform-filter.d.ts +5 -0
- package/dist/test-file-platform-filter.d.ts.map +1 -0
- package/dist/test-file-platform-filter.js +40 -0
- package/dist/toTestResult.d.ts +1 -0
- package/dist/toTestResult.d.ts.map +1 -1
- package/dist/toTestResult.js +2 -2
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/__tests__/bridge.test.ts +37 -2
- package/src/__tests__/crash-monitor.test.ts +182 -231
- package/src/__tests__/errors.test.ts +75 -0
- package/src/__tests__/execute-run.test.ts +235 -34
- package/src/__tests__/harness-session.test.ts +93 -0
- package/src/__tests__/index.test.ts +89 -11
- package/src/__tests__/test-file-platform-filter.test.ts +83 -0
- package/src/crash-monitor.ts +192 -131
- package/src/errors.ts +62 -1
- package/src/execute-run.ts +125 -11
- package/src/harness-session.ts +177 -42
- package/src/index.ts +40 -11
- package/src/run.ts +1 -0
- package/src/test-file-platform-filter.ts +57 -0
- package/src/toTestResult.ts +3 -2
package/src/harness-session.ts
CHANGED
|
@@ -7,11 +7,13 @@ import {
|
|
|
7
7
|
HARNESS_BRIDGE_PATH,
|
|
8
8
|
type HarnessContext,
|
|
9
9
|
type BridgeEvents,
|
|
10
|
+
type TestRunnerEvents,
|
|
10
11
|
type TestExecutionOptions,
|
|
11
12
|
type TestSuiteResult,
|
|
12
13
|
} from '@react-native-harness/bridge';
|
|
13
14
|
import {
|
|
14
15
|
type AppLaunchOptions,
|
|
16
|
+
type AppSession,
|
|
15
17
|
type HarnessPlatform,
|
|
16
18
|
type HarnessPlatformInitOptions,
|
|
17
19
|
type HarnessPlatformRunner,
|
|
@@ -33,8 +35,8 @@ import {
|
|
|
33
35
|
type HarnessRunSummary,
|
|
34
36
|
} from '@react-native-harness/plugins';
|
|
35
37
|
import {
|
|
36
|
-
logger,
|
|
37
38
|
createCrashArtifactWriter,
|
|
39
|
+
logger,
|
|
38
40
|
getTimeoutSignal,
|
|
39
41
|
raceAbortSignals,
|
|
40
42
|
} from '@react-native-harness/tools';
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
} from '@react-native-harness/config';
|
|
46
48
|
import type { Config as JestConfig } from 'jest-runner';
|
|
47
49
|
import { preRunMessage } from 'jest-util';
|
|
50
|
+
import path from 'node:path';
|
|
48
51
|
import { PlatformReadyTimeoutError } from './errors.js';
|
|
49
52
|
import { NoRunnerSpecifiedError, RunnerNotFoundError } from './errors.js';
|
|
50
53
|
import { createCrashMonitor, type CrashMonitor } from './crash-monitor.js';
|
|
@@ -75,6 +78,48 @@ import {
|
|
|
75
78
|
const sessionLogger = logger.child('runtime');
|
|
76
79
|
const defaultResourceLockManager = createResourceLockManager();
|
|
77
80
|
const ignorePromiseRejection = () => undefined;
|
|
81
|
+
const isBridgeDisconnectError = (error: unknown) =>
|
|
82
|
+
error instanceof Error && error.message === 'App bridge disconnected';
|
|
83
|
+
const TEST_RUN_BRIDGE_STABILITY_WAIT_MS = 500;
|
|
84
|
+
|
|
85
|
+
type DisconnectObservableBridge = {
|
|
86
|
+
readonly connection: AppConnection | null;
|
|
87
|
+
on: (event: 'disconnected', listener: () => void) => void;
|
|
88
|
+
off: (event: 'disconnected', listener: () => void) => void;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const waitForBridgeDisconnectOrTimeout = async ({
|
|
92
|
+
bridge,
|
|
93
|
+
connection,
|
|
94
|
+
timeoutMs,
|
|
95
|
+
}: {
|
|
96
|
+
bridge: DisconnectObservableBridge;
|
|
97
|
+
connection: AppConnection;
|
|
98
|
+
timeoutMs: number;
|
|
99
|
+
}): Promise<boolean> => {
|
|
100
|
+
if (bridge.connection !== connection) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return await new Promise<boolean>((resolve) => {
|
|
105
|
+
const onDisconnected = () => {
|
|
106
|
+
cleanup();
|
|
107
|
+
resolve(true);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const cleanup = () => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
bridge.off('disconnected', onDisconnected);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
cleanup();
|
|
117
|
+
resolve(bridge.connection !== connection);
|
|
118
|
+
}, timeoutMs);
|
|
119
|
+
|
|
120
|
+
bridge.on('disconnected', onDisconnected);
|
|
121
|
+
});
|
|
122
|
+
};
|
|
78
123
|
|
|
79
124
|
export type HarnessRunState = {
|
|
80
125
|
readonly runId: string;
|
|
@@ -92,6 +137,7 @@ export type HarnessRunTestsOptions = Exclude<TestExecutionOptions, 'platform'>;
|
|
|
92
137
|
export type HarnessSession = {
|
|
93
138
|
readonly config: HarnessConfig;
|
|
94
139
|
readonly context: HarnessContext;
|
|
140
|
+
onTestRunnerEvent: (listener: (event: TestRunnerEvents) => void) => () => void;
|
|
95
141
|
runTestFile: (path: string, options: HarnessRunTestsOptions) => Promise<TestSuiteResult>;
|
|
96
142
|
ensureAppReady: (testFilePath: string) => Promise<void>;
|
|
97
143
|
restartApp: (testFilePath?: string) => Promise<void>;
|
|
@@ -147,13 +193,37 @@ const withPlatformReadyTimeout = async <T>(options: {
|
|
|
147
193
|
type AppReadyOptions = {
|
|
148
194
|
metroInstance: MetroInstance;
|
|
149
195
|
bridge: HarnessBridge;
|
|
150
|
-
platformInstance: HarnessPlatformRunner;
|
|
151
196
|
platformId: string;
|
|
152
197
|
bundleStartTimeout: number;
|
|
153
198
|
readyTimeout: number;
|
|
154
199
|
maxAppRestarts: number;
|
|
155
200
|
crashMonitor: CrashMonitor;
|
|
156
|
-
|
|
201
|
+
detectNativeCrashes: boolean;
|
|
202
|
+
restartAppSession: () => Promise<AppSession>;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const waitForStartupCrash = async ({
|
|
206
|
+
crashMonitor,
|
|
207
|
+
detectNativeCrashes,
|
|
208
|
+
testFilePath,
|
|
209
|
+
signal,
|
|
210
|
+
}: {
|
|
211
|
+
crashMonitor: CrashMonitor;
|
|
212
|
+
detectNativeCrashes: boolean;
|
|
213
|
+
testFilePath: string;
|
|
214
|
+
signal: AbortSignal;
|
|
215
|
+
}) => {
|
|
216
|
+
if (!detectNativeCrashes) {
|
|
217
|
+
return await waitForAbort(signal);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const watch = crashMonitor.watch(testFilePath, 'startup');
|
|
221
|
+
watch.promise.catch(ignorePromiseRejection); // suppress unhandled-rejection when abort wins race
|
|
222
|
+
try {
|
|
223
|
+
return await Promise.race([watch.promise, waitForAbort(signal)]);
|
|
224
|
+
} finally {
|
|
225
|
+
watch.cancel();
|
|
226
|
+
}
|
|
157
227
|
};
|
|
158
228
|
|
|
159
229
|
const waitForAppReady = async (
|
|
@@ -163,13 +233,13 @@ const waitForAppReady = async (
|
|
|
163
233
|
const {
|
|
164
234
|
metroInstance,
|
|
165
235
|
bridge,
|
|
166
|
-
platformInstance,
|
|
167
236
|
platformId,
|
|
168
237
|
bundleStartTimeout,
|
|
169
238
|
readyTimeout,
|
|
170
239
|
maxAppRestarts,
|
|
171
240
|
crashMonitor,
|
|
172
|
-
|
|
241
|
+
detectNativeCrashes,
|
|
242
|
+
restartAppSession,
|
|
173
243
|
} = base;
|
|
174
244
|
|
|
175
245
|
const logWait = (message: string, ...args: unknown[]) =>
|
|
@@ -184,7 +254,7 @@ const waitForAppReady = async (
|
|
|
184
254
|
signal: new AbortController().signal,
|
|
185
255
|
startAttempt: async () => {
|
|
186
256
|
logWait('launching app for %s', testFilePath);
|
|
187
|
-
await
|
|
257
|
+
await restartAppSession();
|
|
188
258
|
logWait('launch request completed, waiting for bridge ready');
|
|
189
259
|
},
|
|
190
260
|
waitForReady: async (signal) => {
|
|
@@ -195,7 +265,7 @@ const waitForAppReady = async (
|
|
|
195
265
|
// connection from a previous run would resolve the promise before startAttempt
|
|
196
266
|
// even restarts the app — leaving bridge.connection null after the restart.
|
|
197
267
|
await new Promise<void>((resolve, reject) => {
|
|
198
|
-
const onConnected = (
|
|
268
|
+
const onConnected = () => { cleanup(); resolve(); };
|
|
199
269
|
const onAbort = () => { cleanup(); reject(signal.reason ?? new DOMException('Aborted', 'AbortError')); };
|
|
200
270
|
const cleanup = () => {
|
|
201
271
|
bridge.off('connected', onConnected);
|
|
@@ -208,14 +278,13 @@ const waitForAppReady = async (
|
|
|
208
278
|
logWait('runtime ready received');
|
|
209
279
|
},
|
|
210
280
|
waitForCrash: async (signal) => {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
281
|
+
logWait('waiting for crash or runtime ready');
|
|
282
|
+
return await waitForStartupCrash({
|
|
283
|
+
crashMonitor,
|
|
284
|
+
detectNativeCrashes,
|
|
285
|
+
testFilePath,
|
|
286
|
+
signal,
|
|
287
|
+
});
|
|
219
288
|
},
|
|
220
289
|
onAttemptStart: () => {
|
|
221
290
|
logWait('beginning launch attempt for %s', testFilePath);
|
|
@@ -413,6 +482,11 @@ export const createHarnessSession = async (
|
|
|
413
482
|
const clientLogCollector = createClientLogCollector();
|
|
414
483
|
|
|
415
484
|
const context: HarnessContext = { platform };
|
|
485
|
+
const crashArtifactWriter = createCrashArtifactWriter({
|
|
486
|
+
runnerName: platform.name,
|
|
487
|
+
platformId: platform.platformId,
|
|
488
|
+
rootDir: path.join(projectRoot, '.harness', 'crash-reports'),
|
|
489
|
+
});
|
|
416
490
|
|
|
417
491
|
const bridge = await createHarnessBridge({
|
|
418
492
|
noServer: true,
|
|
@@ -446,6 +520,7 @@ export const createHarnessSession = async (
|
|
|
446
520
|
return await import(platform.runner).then((module) =>
|
|
447
521
|
module.default(platform.config, runtimeConfig, {
|
|
448
522
|
signal,
|
|
523
|
+
crashArtifactWriter,
|
|
449
524
|
} satisfies HarnessPlatformInitOptions),
|
|
450
525
|
).then((instance) => {
|
|
451
526
|
sessionLogger.debug('platform runner initialized');
|
|
@@ -460,27 +535,42 @@ export const createHarnessSession = async (
|
|
|
460
535
|
throw error;
|
|
461
536
|
}
|
|
462
537
|
|
|
463
|
-
const crashArtifactWriter = createCrashArtifactWriter({
|
|
464
|
-
runnerName: platform.name,
|
|
465
|
-
platformId: platform.platformId,
|
|
466
|
-
});
|
|
467
|
-
const appMonitor = platformInstance.createAppMonitor({ crashArtifactWriter });
|
|
468
538
|
const appLaunchOptions = (platform.config as { appLaunchOptions?: AppLaunchOptions }).appLaunchOptions;
|
|
469
539
|
|
|
470
|
-
|
|
540
|
+
let currentAppSession: AppSession | null = null;
|
|
541
|
+
const crashMonitor = createCrashMonitor();
|
|
542
|
+
|
|
543
|
+
const disposeCurrentAppSession = async () => {
|
|
544
|
+
const session = currentAppSession;
|
|
545
|
+
currentAppSession = null;
|
|
546
|
+
crashMonitor.setAppSession(null);
|
|
547
|
+
if (session) {
|
|
548
|
+
await session.dispose();
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const restartAppSession = async (): Promise<AppSession> => {
|
|
553
|
+
await crashMonitor.stop();
|
|
554
|
+
await disposeCurrentAppSession();
|
|
555
|
+
const session = await platformInstance.createAppSession(appLaunchOptions);
|
|
556
|
+
currentAppSession = session;
|
|
557
|
+
crashMonitor.setAppSession(session);
|
|
558
|
+
await crashMonitor.start();
|
|
559
|
+
return session;
|
|
560
|
+
};
|
|
471
561
|
|
|
472
562
|
// Pre-build the options that are constant across all app-ready calls;
|
|
473
563
|
// only testFilePath varies per call.
|
|
474
564
|
const appReadyBaseOptions: AppReadyOptions = {
|
|
475
565
|
metroInstance,
|
|
476
566
|
bridge,
|
|
477
|
-
platformInstance,
|
|
478
567
|
platformId: platform.platformId,
|
|
479
568
|
bundleStartTimeout: runtimeConfig.bundleStartTimeout ?? 60000,
|
|
480
569
|
readyTimeout: runtimeConfig.bridgeTimeout,
|
|
481
570
|
maxAppRestarts: runtimeConfig.maxAppRestarts ?? 2,
|
|
482
571
|
crashMonitor,
|
|
483
|
-
|
|
572
|
+
detectNativeCrashes: runtimeConfig.detectNativeCrashes !== false,
|
|
573
|
+
restartAppSession,
|
|
484
574
|
};
|
|
485
575
|
|
|
486
576
|
// --- Event listeners ---
|
|
@@ -497,6 +587,19 @@ export const createHarnessSession = async (
|
|
|
497
587
|
const flushClientLogs = (): ClientLogBuffer => clientLogCollector.flush();
|
|
498
588
|
|
|
499
589
|
const clientLogListener = clientLogCollector.handleEvent;
|
|
590
|
+
const testRunnerEventListeners = new Set<(event: TestRunnerEvents) => void>();
|
|
591
|
+
const onTestRunnerEvent = (event: BridgeEvents) => {
|
|
592
|
+
if (
|
|
593
|
+
event.type === 'test-started' ||
|
|
594
|
+
event.type === 'test-finished' ||
|
|
595
|
+
event.type === 'suite-started' ||
|
|
596
|
+
event.type === 'suite-finished' ||
|
|
597
|
+
event.type === 'file-started' ||
|
|
598
|
+
event.type === 'file-finished'
|
|
599
|
+
) {
|
|
600
|
+
testRunnerEventListeners.forEach((listener) => listener(event));
|
|
601
|
+
}
|
|
602
|
+
};
|
|
500
603
|
|
|
501
604
|
const onConnected = (conn: AppConnection) => {
|
|
502
605
|
const runId = getCurrentRunId();
|
|
@@ -508,11 +611,15 @@ export const createHarnessSession = async (
|
|
|
508
611
|
const runId = getCurrentRunId();
|
|
509
612
|
if (!runId) return;
|
|
510
613
|
hooks.schedule(() => pluginManager.callHook('runtime:disconnected', { runId, reason: 'bridge-disconnected' }));
|
|
614
|
+
if (runtimeConfig.detectNativeCrashes !== false) {
|
|
615
|
+
crashMonitor.handleBridgeDisconnect();
|
|
616
|
+
}
|
|
511
617
|
};
|
|
512
618
|
|
|
513
619
|
bridge.on('connected', onConnected);
|
|
514
620
|
bridge.on('disconnected', onDisconnected);
|
|
515
621
|
bridge.on('event', bridgeEventListener);
|
|
622
|
+
bridge.on('event', onTestRunnerEvent);
|
|
516
623
|
metroInstance.events.addListener(onMetroEvent);
|
|
517
624
|
if (runtimeConfig.forwardClientLogs) {
|
|
518
625
|
metroInstance.events.addListener(clientLogListener);
|
|
@@ -557,11 +664,13 @@ export const createHarnessSession = async (
|
|
|
557
664
|
bridge.off('connected', onConnected);
|
|
558
665
|
bridge.off('disconnected', onDisconnected);
|
|
559
666
|
bridge.off('event', bridgeEventListener);
|
|
667
|
+
bridge.off('event', onTestRunnerEvent);
|
|
560
668
|
|
|
561
669
|
const nativeCoverageConfig = runtimeConfig.coverage?.native?.ios;
|
|
562
670
|
if (nativeCoverageConfig?.pods?.length && platformInstance.collectNativeCoverage) {
|
|
563
671
|
try {
|
|
564
|
-
await
|
|
672
|
+
await crashMonitor.stop();
|
|
673
|
+
await disposeCurrentAppSession();
|
|
565
674
|
const lcovPath = await platformInstance.collectNativeCoverage({
|
|
566
675
|
pods: nativeCoverageConfig.pods,
|
|
567
676
|
outputDir: projectRoot,
|
|
@@ -578,6 +687,7 @@ export const createHarnessSession = async (
|
|
|
578
687
|
try {
|
|
579
688
|
await Promise.all([
|
|
580
689
|
crashMonitor.dispose(),
|
|
690
|
+
disposeCurrentAppSession(),
|
|
581
691
|
bridge.dispose(),
|
|
582
692
|
platformInstance.dispose(),
|
|
583
693
|
metroInstance.dispose(),
|
|
@@ -614,8 +724,6 @@ export const createHarnessSession = async (
|
|
|
614
724
|
try {
|
|
615
725
|
await pluginManager.callHook('harness:before-creation', { appLaunchOptions });
|
|
616
726
|
await hooks.drain();
|
|
617
|
-
await appMonitor.start();
|
|
618
|
-
sessionLogger.debug('app monitor started');
|
|
619
727
|
await pluginManager.callHook('harness:before-run', { appLaunchOptions });
|
|
620
728
|
await hooks.drain();
|
|
621
729
|
} catch (error) {
|
|
@@ -634,9 +742,12 @@ export const createHarnessSession = async (
|
|
|
634
742
|
await hooks.drain();
|
|
635
743
|
sessionLogger.debug('ensuring app is ready for %s', testFilePath);
|
|
636
744
|
|
|
637
|
-
if (crashMonitor.isAlive() && bridge.connection !== null &&
|
|
638
|
-
|
|
639
|
-
|
|
745
|
+
if (crashMonitor.isAlive() && bridge.connection !== null && currentAppSession) {
|
|
746
|
+
const state = await currentAppSession.getState();
|
|
747
|
+
if (state.status === 'running') {
|
|
748
|
+
sessionLogger.debug('reusing existing ready app for %s', testFilePath);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
640
751
|
}
|
|
641
752
|
|
|
642
753
|
crashMonitor.reset();
|
|
@@ -655,17 +766,16 @@ export const createHarnessSession = async (
|
|
|
655
766
|
testFilePath ? 'stop-and-ensure-ready' : 'direct-restart',
|
|
656
767
|
);
|
|
657
768
|
|
|
658
|
-
|
|
659
|
-
await platformInstance.stopApp();
|
|
660
|
-
} else {
|
|
661
|
-
await platformInstance.restartApp(appLaunchOptions);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
crashMonitor.reset();
|
|
665
|
-
await crashMonitor.start();
|
|
769
|
+
await disposeCurrentAppSession();
|
|
666
770
|
|
|
667
771
|
if (testFilePath) {
|
|
668
772
|
await ensureAppReady(testFilePath);
|
|
773
|
+
} else {
|
|
774
|
+
const session = await platformInstance.createAppSession(appLaunchOptions);
|
|
775
|
+
currentAppSession = session;
|
|
776
|
+
crashMonitor.setAppSession(session);
|
|
777
|
+
crashMonitor.reset();
|
|
778
|
+
await crashMonitor.start();
|
|
669
779
|
}
|
|
670
780
|
|
|
671
781
|
await hooks.drain();
|
|
@@ -681,7 +791,7 @@ export const createHarnessSession = async (
|
|
|
681
791
|
if (!conn) throw new Error('No active app connection');
|
|
682
792
|
sessionLogger.debug('running test file on client: %s', testPath);
|
|
683
793
|
|
|
684
|
-
if (
|
|
794
|
+
if (runtimeConfig.detectNativeCrashes === false) {
|
|
685
795
|
const result = await conn.runTests(testPath, { ...options, runner: platform.runner });
|
|
686
796
|
await hooks.drain();
|
|
687
797
|
return result;
|
|
@@ -692,12 +802,31 @@ export const createHarnessSession = async (
|
|
|
692
802
|
// crash wins the race or cancel() is called after the test run wins.
|
|
693
803
|
crashWatch.promise.catch(ignorePromiseRejection);
|
|
694
804
|
try {
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
805
|
+
const testRunPromise = conn.runTests(testPath, {
|
|
806
|
+
...options,
|
|
807
|
+
runner: platform.runner,
|
|
808
|
+
});
|
|
809
|
+
const result = await Promise.race([testRunPromise, crashWatch.promise]);
|
|
810
|
+
const bridgeDisconnected = await waitForBridgeDisconnectOrTimeout({
|
|
811
|
+
bridge,
|
|
812
|
+
connection: conn,
|
|
813
|
+
timeoutMs: TEST_RUN_BRIDGE_STABILITY_WAIT_MS,
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
if (bridgeDisconnected) {
|
|
817
|
+
return await crashWatch.promise;
|
|
818
|
+
}
|
|
819
|
+
|
|
699
820
|
await hooks.drain();
|
|
700
821
|
return result;
|
|
822
|
+
} catch (error) {
|
|
823
|
+
if (isBridgeDisconnectError(error)) {
|
|
824
|
+
const result = await crashWatch.promise;
|
|
825
|
+
await hooks.drain();
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
throw error;
|
|
701
830
|
} finally {
|
|
702
831
|
crashWatch.cancel();
|
|
703
832
|
}
|
|
@@ -706,6 +835,12 @@ export const createHarnessSession = async (
|
|
|
706
835
|
return {
|
|
707
836
|
config: runtimeConfig,
|
|
708
837
|
context,
|
|
838
|
+
onTestRunnerEvent: (listener) => {
|
|
839
|
+
testRunnerEventListeners.add(listener);
|
|
840
|
+
return () => {
|
|
841
|
+
testRunnerEventListeners.delete(listener);
|
|
842
|
+
};
|
|
843
|
+
},
|
|
709
844
|
runTestFile,
|
|
710
845
|
ensureAppReady,
|
|
711
846
|
restartApp,
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
CallbackTestRunnerInterface,
|
|
3
2
|
Config,
|
|
4
|
-
|
|
5
|
-
OnTestStart,
|
|
6
|
-
OnTestSuccess,
|
|
3
|
+
EmittingTestRunnerInterface,
|
|
7
4
|
Test,
|
|
5
|
+
TestEvents,
|
|
8
6
|
TestRunnerOptions,
|
|
9
7
|
TestWatcher,
|
|
10
8
|
} from 'jest-runner';
|
|
@@ -13,22 +11,41 @@ import { executeRun } from './execute-run.js';
|
|
|
13
11
|
import { HarnessError } from '@react-native-harness/tools';
|
|
14
12
|
import { getErrorMessage } from './logs.js';
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
type TestEventListener<Name extends keyof TestEvents> = (
|
|
15
|
+
eventData: TestEvents[Name],
|
|
16
|
+
) => void | Promise<void>;
|
|
17
|
+
|
|
18
|
+
export default class JestHarness implements EmittingTestRunnerInterface {
|
|
17
19
|
readonly isSerial = true;
|
|
20
|
+
readonly supportsEventEmitters = true as const;
|
|
18
21
|
|
|
19
22
|
#globalConfig: Config.GlobalConfig;
|
|
20
23
|
#session: HarnessSession | null = null;
|
|
24
|
+
#listeners = new Map<keyof TestEvents, Set<TestEventListener<keyof TestEvents>>>();
|
|
21
25
|
|
|
22
26
|
constructor(globalConfig: Config.GlobalConfig) {
|
|
23
27
|
this.#globalConfig = globalConfig;
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
on<Name extends keyof TestEvents>(
|
|
31
|
+
eventName: Name,
|
|
32
|
+
listener: TestEventListener<Name>,
|
|
33
|
+
): () => void {
|
|
34
|
+
const listeners = this.#listeners.get(eventName) ?? new Set();
|
|
35
|
+
listeners.add(listener as TestEventListener<keyof TestEvents>);
|
|
36
|
+
this.#listeners.set(eventName, listeners);
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
listeners.delete(listener as TestEventListener<keyof TestEvents>);
|
|
40
|
+
if (listeners.size === 0) {
|
|
41
|
+
this.#listeners.delete(eventName);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
async runTests(
|
|
27
47
|
tests: Array<Test>,
|
|
28
48
|
watcher: TestWatcher,
|
|
29
|
-
onStart: OnTestStart,
|
|
30
|
-
onResult: OnTestSuccess,
|
|
31
|
-
onFailure: OnTestFailure,
|
|
32
49
|
options: TestRunnerOptions,
|
|
33
50
|
): Promise<void> {
|
|
34
51
|
if (!options.serial) {
|
|
@@ -46,9 +63,7 @@ export default class JestHarness implements CallbackTestRunnerInterface {
|
|
|
46
63
|
this.#session,
|
|
47
64
|
tests,
|
|
48
65
|
watcher,
|
|
49
|
-
|
|
50
|
-
onResult,
|
|
51
|
-
onFailure,
|
|
66
|
+
this.#emit,
|
|
52
67
|
this.#globalConfig,
|
|
53
68
|
);
|
|
54
69
|
} catch (error) {
|
|
@@ -63,4 +78,18 @@ export default class JestHarness implements CallbackTestRunnerInterface {
|
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
}
|
|
81
|
+
|
|
82
|
+
#emit = async <Name extends keyof TestEvents>(
|
|
83
|
+
eventName: Name,
|
|
84
|
+
...eventData: TestEvents[Name]
|
|
85
|
+
): Promise<void> => {
|
|
86
|
+
const listeners = this.#listeners.get(eventName);
|
|
87
|
+
if (!listeners) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const listener of listeners) {
|
|
92
|
+
await listener(eventData);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
66
95
|
}
|
package/src/run.ts
CHANGED
|
@@ -111,6 +111,7 @@ export const runHarnessTestFile: RunHarnessTestFile = async ({
|
|
|
111
111
|
? `${errorMessage}${codeFrame ? `\n\n${codeFrame.content}` : ''}`
|
|
112
112
|
: undefined,
|
|
113
113
|
title: test.name,
|
|
114
|
+
fullName: test.fullName,
|
|
114
115
|
status: test.status,
|
|
115
116
|
location: codeFrame?.location
|
|
116
117
|
? { column: codeFrame.location.column, line: codeFrame.location.row }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import type { TestResult as JestTestResult } from '@jest/test-result';
|
|
3
|
+
import { toTestResult } from './toTestResult.js';
|
|
4
|
+
|
|
5
|
+
const PLATFORM_SPECIFIC_HARNESS_FILE =
|
|
6
|
+
/\.([^.]+)\.harness\.(?:[mc]?[jt]sx?)$/;
|
|
7
|
+
|
|
8
|
+
export const getHarnessTestFilePlatform = (
|
|
9
|
+
testPath: string,
|
|
10
|
+
knownPlatformIds: ReadonlySet<string>,
|
|
11
|
+
): string | null => {
|
|
12
|
+
const match = path.basename(testPath).match(PLATFORM_SPECIFIC_HARNESS_FILE);
|
|
13
|
+
if (!match) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const candidate = match[1];
|
|
18
|
+
return knownPlatformIds.has(candidate) ? candidate : null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const shouldRunHarnessTestFile = (
|
|
22
|
+
testPath: string,
|
|
23
|
+
platformId: string,
|
|
24
|
+
knownPlatformIds: ReadonlySet<string>,
|
|
25
|
+
): boolean => {
|
|
26
|
+
const filePlatform = getHarnessTestFilePlatform(testPath, knownPlatformIds);
|
|
27
|
+
return filePlatform == null || filePlatform === platformId;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const createPlatformSkippedTestResult = (
|
|
31
|
+
testPath: string,
|
|
32
|
+
): JestTestResult => {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const title = path.basename(testPath);
|
|
35
|
+
|
|
36
|
+
return toTestResult({
|
|
37
|
+
stats: {
|
|
38
|
+
failures: 0,
|
|
39
|
+
passes: 0,
|
|
40
|
+
pending: 1,
|
|
41
|
+
todo: 0,
|
|
42
|
+
start: now,
|
|
43
|
+
end: now,
|
|
44
|
+
},
|
|
45
|
+
skipped: true,
|
|
46
|
+
errorMessage: null,
|
|
47
|
+
tests: [
|
|
48
|
+
{
|
|
49
|
+
status: 'skipped',
|
|
50
|
+
title,
|
|
51
|
+
fullName: title,
|
|
52
|
+
testPath,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
jestTestPath: testPath,
|
|
56
|
+
});
|
|
57
|
+
};
|
package/src/toTestResult.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type Options = {
|
|
|
16
16
|
errorMessage?: string;
|
|
17
17
|
testPath?: string;
|
|
18
18
|
title?: string;
|
|
19
|
+
fullName?: string;
|
|
19
20
|
status: Status;
|
|
20
21
|
location?: {
|
|
21
22
|
column: number;
|
|
@@ -72,8 +73,8 @@ const getTestResults = ({
|
|
|
72
73
|
duration: test.duration,
|
|
73
74
|
failureDetails: [],
|
|
74
75
|
failureMessages: actualErrorMessage ? [actualErrorMessage] : [],
|
|
75
|
-
fullName:
|
|
76
|
-
numPassingAsserts: test.
|
|
76
|
+
fullName: test.fullName || test.testPath || jestTestPath || '',
|
|
77
|
+
numPassingAsserts: test.status === 'passed' ? 1 : 0,
|
|
77
78
|
status: test.status,
|
|
78
79
|
title: test.title || '',
|
|
79
80
|
location: test.location,
|