@react-native-harness/jest 1.2.0-rc.1 → 1.3.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__/bridge.test.d.ts +2 -0
- package/dist/__tests__/bridge.test.d.ts.map +1 -0
- package/dist/__tests__/bridge.test.js +142 -0
- package/dist/__tests__/client-log-handler.test.js +116 -72
- package/dist/__tests__/crash-monitor.test.d.ts +2 -0
- package/dist/__tests__/crash-monitor.test.d.ts.map +1 -0
- package/dist/__tests__/crash-monitor.test.js +212 -0
- package/dist/__tests__/execute-run.test.d.ts +2 -0
- package/dist/__tests__/execute-run.test.d.ts.map +1 -0
- package/dist/__tests__/execute-run.test.js +229 -0
- 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 +250 -0
- package/dist/__tests__/hook-queue.test.d.ts +2 -0
- package/dist/__tests__/hook-queue.test.d.ts.map +1 -0
- package/dist/__tests__/hook-queue.test.js +56 -0
- package/dist/__tests__/index.test.js +94 -37
- package/dist/__tests__/resource-lock.test.js +24 -24
- package/dist/client-log-handler.d.ts +10 -11
- package/dist/client-log-handler.d.ts.map +1 -1
- package/dist/client-log-handler.js +36 -58
- package/dist/crash-monitor.d.ts +17 -10
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +164 -63
- package/dist/execute-run.d.ts +4 -0
- package/dist/execute-run.d.ts.map +1 -0
- package/dist/execute-run.js +151 -0
- package/dist/harness-session.d.ts +34 -0
- package/dist/harness-session.d.ts.map +1 -0
- package/dist/harness-session.js +523 -0
- package/dist/hook-queue.d.ts +6 -0
- package/dist/hook-queue.d.ts.map +1 -0
- package/dist/hook-queue.js +18 -0
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -230
- package/dist/logs.d.ts +1 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +3 -0
- package/dist/resource-lock.d.ts +0 -1
- package/dist/resource-lock.d.ts.map +1 -1
- package/dist/resource-lock.js +1 -1
- package/dist/run.d.ts +2 -2
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +3 -3
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +7 -8
- package/src/__tests__/bridge.test.ts +184 -0
- package/src/__tests__/client-log-handler.test.ts +117 -81
- package/src/__tests__/crash-monitor.test.ts +288 -0
- package/src/__tests__/execute-run.test.ts +325 -0
- package/src/__tests__/hook-queue.test.ts +77 -0
- package/src/__tests__/index.test.ts +135 -55
- package/src/__tests__/resource-lock.test.ts +29 -42
- package/src/client-log-handler.ts +48 -64
- package/src/crash-monitor.ts +248 -0
- package/src/execute-run.ts +199 -0
- package/src/harness-session.ts +734 -0
- package/src/hook-queue.ts +26 -0
- package/src/index.ts +17 -330
- package/src/logs.ts +4 -0
- package/src/resource-lock.ts +1 -1
- package/src/run.ts +5 -5
- package/tsconfig.tsbuildinfo +1 -0
- package/src/__tests__/crash-supervisor.test.ts +0 -200
- package/src/__tests__/harness-cache.test.ts +0 -74
- package/src/__tests__/harness.test.ts +0 -923
- package/src/crash-supervisor.ts +0 -321
- package/src/env.d.ts +0 -9
- package/src/harness.ts +0 -870
- package/src/setup.ts +0 -127
- package/src/teardown.ts +0 -14
|
@@ -1,41 +1,19 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import util from 'node:util';
|
|
3
|
-
import { log } from './logs.js';
|
|
4
2
|
/**
|
|
5
|
-
* Gets the
|
|
3
|
+
* Gets the Jest console level for a client log level.
|
|
6
4
|
* Note: Metro treats 'trace' as 'log' because Hermes doesn't include stack traces.
|
|
7
5
|
*/
|
|
8
|
-
const
|
|
9
|
-
// Metro converts trace to log for display (Hermes doesn't provide stack traces)
|
|
10
|
-
if (level === 'trace') {
|
|
11
|
-
return 'LOG';
|
|
12
|
-
}
|
|
13
|
-
return level.toUpperCase();
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Creates a styled tag for a log level with colored box appearance.
|
|
17
|
-
*/
|
|
18
|
-
const createLevelTag = (level) => {
|
|
19
|
-
const displayLevel = getDisplayLevel(level);
|
|
20
|
-
const label = ` ${displayLevel} `;
|
|
21
|
-
if (!chalk.supportsColor) {
|
|
22
|
-
return `[${displayLevel}]`;
|
|
23
|
-
}
|
|
6
|
+
const getJestLogLevel = (level) => {
|
|
24
7
|
switch (level) {
|
|
25
|
-
case '
|
|
26
|
-
|
|
27
|
-
case '
|
|
28
|
-
return
|
|
29
|
-
case 'info':
|
|
30
|
-
return chalk.reset.inverse.bold.cyan(label);
|
|
31
|
-
case 'debug':
|
|
32
|
-
return chalk.reset.inverse.bold.blue(label);
|
|
8
|
+
case 'group':
|
|
9
|
+
case 'groupCollapsed':
|
|
10
|
+
case 'groupEnd':
|
|
11
|
+
return null;
|
|
33
12
|
case 'trace':
|
|
34
|
-
|
|
35
|
-
return chalk.reset.inverse.bold.magenta(label);
|
|
13
|
+
return 'log';
|
|
36
14
|
case 'log':
|
|
37
15
|
default:
|
|
38
|
-
return
|
|
16
|
+
return level;
|
|
39
17
|
}
|
|
40
18
|
};
|
|
41
19
|
/**
|
|
@@ -49,36 +27,36 @@ export const formatClientLogMessage = (data) => {
|
|
|
49
27
|
return util.format(...data);
|
|
50
28
|
};
|
|
51
29
|
/**
|
|
52
|
-
* Formats a client log event into
|
|
53
|
-
*/
|
|
54
|
-
export const formatClientLogLine = (event) => {
|
|
55
|
-
const tag = createLevelTag(event.level);
|
|
56
|
-
const message = formatClientLogMessage(event.data);
|
|
57
|
-
return `${tag} ${message}`;
|
|
58
|
-
};
|
|
59
|
-
/**
|
|
60
|
-
* Handles a client_log event by formatting and logging it.
|
|
61
|
-
* Returns true if the event was handled, false otherwise.
|
|
30
|
+
* Formats a client log event into Jest's buffered console entry format.
|
|
62
31
|
*/
|
|
63
|
-
export const
|
|
64
|
-
|
|
65
|
-
|
|
32
|
+
export const formatClientLogEntry = (event) => {
|
|
33
|
+
const logLevel = getJestLogLevel(event.level);
|
|
34
|
+
if (!logLevel) {
|
|
35
|
+
return null;
|
|
66
36
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
const logLine = formatClientLogLine(event);
|
|
74
|
-
log(logLine);
|
|
75
|
-
return true;
|
|
37
|
+
const message = formatClientLogMessage(event.data);
|
|
38
|
+
return {
|
|
39
|
+
message,
|
|
40
|
+
origin: '',
|
|
41
|
+
type: logLevel,
|
|
42
|
+
};
|
|
76
43
|
};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
44
|
+
export const createClientLogCollector = () => {
|
|
45
|
+
let buffer = [];
|
|
46
|
+
return {
|
|
47
|
+
handleEvent: (event) => {
|
|
48
|
+
if (event.type !== 'client_log') {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const entry = formatClientLogEntry(event);
|
|
52
|
+
if (entry) {
|
|
53
|
+
buffer.push(entry);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
flush: () => {
|
|
57
|
+
const flushed = buffer;
|
|
58
|
+
buffer = [];
|
|
59
|
+
return flushed;
|
|
60
|
+
},
|
|
83
61
|
};
|
|
84
62
|
};
|
package/dist/crash-monitor.d.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import { HarnessPlatformRunner } from '@react-native-harness/platforms';
|
|
2
|
-
import {
|
|
1
|
+
import { type AppMonitor, type HarnessPlatformRunner } from '@react-native-harness/platforms';
|
|
2
|
+
import { type NativeCrashPhase } from './errors.js';
|
|
3
|
+
export declare class CrashWatchCancelledError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
export type CrashWatch = {
|
|
7
|
+
readonly promise: Promise<never>;
|
|
8
|
+
cancel: () => void;
|
|
9
|
+
};
|
|
3
10
|
export type CrashMonitor = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
watch: (testFilePath: string, phase: NativeCrashPhase) => CrashWatch;
|
|
12
|
+
isAlive: () => boolean;
|
|
13
|
+
stop: () => Promise<void>;
|
|
14
|
+
start: () => Promise<void>;
|
|
15
|
+
reset: () => void;
|
|
16
|
+
dispose: () => Promise<void>;
|
|
9
17
|
};
|
|
10
18
|
export type CrashMonitorOptions = {
|
|
11
|
-
|
|
19
|
+
appMonitor: AppMonitor;
|
|
12
20
|
platformRunner: HarnessPlatformRunner;
|
|
13
|
-
bridgeServer: BridgeServer;
|
|
14
21
|
};
|
|
15
|
-
export declare const createCrashMonitor: ({
|
|
22
|
+
export declare const createCrashMonitor: ({ appMonitor, platformRunner, }: CrashMonitorOptions) => CrashMonitor;
|
|
16
23
|
//# sourceMappingURL=crash-monitor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crash-monitor.d.ts","sourceRoot":"","sources":["../src/crash-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"crash-monitor.d.ts","sourceRoot":"","sources":["../src/crash-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAIf,KAAK,qBAAqB,EAC3B,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,aAAa,CAAC;AAKrB,qBAAa,wBAAyB,SAAQ,KAAK;;CAKlD;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,KAAK,UAAU,CAAC;IACrE,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,qBAAqB,CAAC;CACvC,CAAC;AA6BF,eAAO,MAAM,kBAAkB,GAAI,iCAGhC,mBAAmB,KAAG,YA+KxB,CAAC"}
|
package/dist/crash-monitor.js
CHANGED
|
@@ -1,75 +1,176 @@
|
|
|
1
|
-
import { NativeCrashError } from './errors.js';
|
|
1
|
+
import { NativeCrashError, } from './errors.js';
|
|
2
2
|
import { logger } from '@react-native-harness/tools';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
3
|
+
const crashLogger = logger.child('crash');
|
|
4
|
+
export class CrashWatchCancelledError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super('Crash watch was cancelled');
|
|
7
|
+
this.name = 'CrashWatchCancelledError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const mergeCrashDetails = (phase, initial, enriched, fallbackSummary) => ({
|
|
11
|
+
phase,
|
|
12
|
+
source: enriched?.source ?? initial?.source,
|
|
13
|
+
summary: enriched?.summary ?? initial?.summary ?? fallbackSummary,
|
|
14
|
+
signal: enriched?.signal ?? initial?.signal,
|
|
15
|
+
exceptionType: enriched?.exceptionType ?? initial?.exceptionType,
|
|
16
|
+
processName: enriched?.processName ?? initial?.processName,
|
|
17
|
+
pid: enriched?.pid ?? initial?.pid,
|
|
18
|
+
stackTrace: enriched?.stackTrace ?? initial?.stackTrace,
|
|
19
|
+
rawLines: enriched?.rawLines ?? initial?.rawLines,
|
|
20
|
+
artifactType: enriched?.artifactType ?? initial?.artifactType,
|
|
21
|
+
artifactPath: enriched?.artifactPath ?? initial?.artifactPath,
|
|
22
|
+
});
|
|
23
|
+
export const createCrashMonitor = ({ appMonitor, platformRunner, }) => {
|
|
24
|
+
let alive = false;
|
|
25
|
+
let monitoring = true;
|
|
26
|
+
let isResolvingCrash = false;
|
|
27
|
+
let disposed = false;
|
|
28
|
+
// Both updated when watch() is called so crashes are attributed to the
|
|
29
|
+
// correct test file and lifecycle phase.
|
|
30
|
+
let currentTestFilePath = '';
|
|
31
|
+
let currentPhase = 'startup';
|
|
32
|
+
const watchers = new Set();
|
|
33
|
+
const getCrashDetailsProvider = () => {
|
|
34
|
+
if ('getCrashDetails' in appMonitor) {
|
|
35
|
+
return appMonitor;
|
|
22
36
|
}
|
|
37
|
+
if (platformRunner.getCrashDetails) {
|
|
38
|
+
return platformRunner;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
23
41
|
};
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
bridgeServer.on('disconnect', handleDisconnect);
|
|
30
|
-
// Poll for app running status
|
|
31
|
-
pollingInterval = setInterval(async () => {
|
|
32
|
-
// Skip check during intentional restarts
|
|
33
|
-
if (isIntentionalRestart) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const isRunning = await platformRunner.isAppRunning();
|
|
38
|
-
if (!isRunning && currentTestFilePath) {
|
|
39
|
-
logger.debug(`Native crash detected during: ${currentTestFilePath}`);
|
|
40
|
-
stopMonitoring();
|
|
41
|
-
reject(new NativeCrashError(currentTestFilePath));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
logger.error('Error checking app status:', error);
|
|
46
|
-
}
|
|
47
|
-
}, interval);
|
|
48
|
-
});
|
|
42
|
+
const notifyCrash = (err) => {
|
|
43
|
+
const pending = [...watchers];
|
|
44
|
+
watchers.clear();
|
|
45
|
+
for (const fn of pending)
|
|
46
|
+
fn(err);
|
|
49
47
|
};
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
const handleCrash = async (phase, details, fallbackSummary) => {
|
|
49
|
+
if (isResolvingCrash)
|
|
50
|
+
return;
|
|
51
|
+
isResolvingCrash = true;
|
|
52
|
+
alive = false;
|
|
53
|
+
crashLogger.debug('native crash detected (phase=%s)', phase);
|
|
54
|
+
for (const line of details?.rawLines ?? []) {
|
|
55
|
+
crashLogger.debug('%s', line);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const enriched = await getCrashDetailsProvider()?.getCrashDetails?.({
|
|
59
|
+
processName: details?.processName,
|
|
60
|
+
pid: details?.pid,
|
|
61
|
+
occurredAt: Date.now(),
|
|
62
|
+
});
|
|
63
|
+
const merged = mergeCrashDetails(phase, details, enriched, fallbackSummary);
|
|
64
|
+
crashLogger.debug('crash details: %o', {
|
|
65
|
+
phase: merged.phase,
|
|
66
|
+
source: merged.source,
|
|
67
|
+
summary: merged.summary,
|
|
68
|
+
signal: merged.signal,
|
|
69
|
+
exceptionType: merged.exceptionType,
|
|
70
|
+
processName: merged.processName,
|
|
71
|
+
pid: merged.pid,
|
|
72
|
+
});
|
|
73
|
+
notifyCrash(new NativeCrashError(currentTestFilePath, merged));
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
isResolvingCrash = false;
|
|
54
77
|
}
|
|
55
|
-
bridgeServer.off('disconnect', handleDisconnect);
|
|
56
|
-
currentTestFilePath = null;
|
|
57
|
-
rejectFn = null;
|
|
58
78
|
};
|
|
59
|
-
const
|
|
60
|
-
|
|
79
|
+
const confirmAndHandleCrash = async (phase, details, fallbackSummary) => {
|
|
80
|
+
if (disposed || !monitoring)
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
const isRunning = await platformRunner.isAppRunning();
|
|
84
|
+
if (!isRunning) {
|
|
85
|
+
void handleCrash(phase, details, fallbackSummary);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
crashLogger.debug('crash confirmation failed', error);
|
|
90
|
+
}
|
|
61
91
|
};
|
|
62
|
-
const
|
|
63
|
-
|
|
92
|
+
const extractCrashDetails = (event) => event.crashDetails
|
|
93
|
+
? {
|
|
94
|
+
source: event.crashDetails.source ?? event.source,
|
|
95
|
+
summary: event.crashDetails.summary,
|
|
96
|
+
signal: event.crashDetails.signal,
|
|
97
|
+
exceptionType: event.crashDetails.exceptionType,
|
|
98
|
+
processName: event.crashDetails.processName,
|
|
99
|
+
pid: event.crashDetails.pid ?? event.pid,
|
|
100
|
+
stackTrace: event.crashDetails.stackTrace,
|
|
101
|
+
rawLines: event.crashDetails.rawLines ??
|
|
102
|
+
(event.line ? [event.line] : undefined),
|
|
103
|
+
}
|
|
104
|
+
: undefined;
|
|
105
|
+
const appMonitorListener = (event) => {
|
|
106
|
+
if (disposed || !monitoring)
|
|
107
|
+
return;
|
|
108
|
+
if (event.type === 'app_started') {
|
|
109
|
+
alive = true;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (event.type === 'app_exited') {
|
|
113
|
+
const details = extractCrashDetails(event);
|
|
114
|
+
if (event.isConfirmed ?? event.source === 'polling') {
|
|
115
|
+
void handleCrash(currentPhase, details);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
void confirmAndHandleCrash(currentPhase, details);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (event.type === 'possible_crash') {
|
|
123
|
+
const details = extractCrashDetails(event);
|
|
124
|
+
const fallback = `possible crash signal (${event.source ?? 'unknown'})`;
|
|
125
|
+
if (event.isConfirmed) {
|
|
126
|
+
void handleCrash(currentPhase, details, fallback);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
void confirmAndHandleCrash(currentPhase, details, fallback);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
64
132
|
};
|
|
65
|
-
|
|
66
|
-
|
|
133
|
+
appMonitor.addListener(appMonitorListener);
|
|
134
|
+
const watch = (testFilePath, phase) => {
|
|
135
|
+
currentTestFilePath = testFilePath;
|
|
136
|
+
currentPhase = phase;
|
|
137
|
+
let rejectFn;
|
|
138
|
+
const promise = new Promise((_, reject) => {
|
|
139
|
+
rejectFn = (err) => {
|
|
140
|
+
watchers.delete(rejectFn);
|
|
141
|
+
reject(err);
|
|
142
|
+
};
|
|
143
|
+
watchers.add(rejectFn);
|
|
144
|
+
});
|
|
145
|
+
const cancel = () => {
|
|
146
|
+
rejectFn(new CrashWatchCancelledError());
|
|
147
|
+
};
|
|
148
|
+
return { promise, cancel };
|
|
67
149
|
};
|
|
68
150
|
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
151
|
+
watch,
|
|
152
|
+
isAlive: () => alive,
|
|
153
|
+
stop: async () => {
|
|
154
|
+
monitoring = false;
|
|
155
|
+
await appMonitor.stop();
|
|
156
|
+
},
|
|
157
|
+
start: async () => {
|
|
158
|
+
monitoring = true;
|
|
159
|
+
await appMonitor.start();
|
|
160
|
+
},
|
|
161
|
+
reset: () => {
|
|
162
|
+
alive = false;
|
|
163
|
+
watchers.clear();
|
|
164
|
+
isResolvingCrash = false;
|
|
165
|
+
currentTestFilePath = '';
|
|
166
|
+
},
|
|
167
|
+
dispose: async () => {
|
|
168
|
+
disposed = true;
|
|
169
|
+
monitoring = false;
|
|
170
|
+
watchers.clear();
|
|
171
|
+
isResolvingCrash = false;
|
|
172
|
+
appMonitor.removeListener(appMonitorListener);
|
|
173
|
+
await appMonitor.dispose();
|
|
174
|
+
},
|
|
74
175
|
};
|
|
75
176
|
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OnTestFailure, OnTestStart, OnTestSuccess, Test, TestWatcher, Config } from 'jest-runner';
|
|
2
|
+
import { type HarnessSession } from './harness-session.js';
|
|
3
|
+
export declare const executeRun: (session: HarnessSession, tests: Array<Test>, watcher: TestWatcher, onStart: OnTestStart, onResult: OnTestSuccess, onFailure: OnTestFailure, globalConfig: Config.GlobalConfig) => Promise<void>;
|
|
4
|
+
//# sourceMappingURL=execute-run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute-run.d.ts","sourceRoot":"","sources":["../src/execute-run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,WAAW,EACX,aAAa,EACb,IAAI,EACJ,WAAW,EACX,MAAM,EACP,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,KAAK,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AAsCjF,eAAO,MAAM,UAAU,GACrB,SAAS,cAAc,EACvB,OAAO,KAAK,CAAC,IAAI,CAAC,EAClB,SAAS,WAAW,EACpB,SAAS,WAAW,EACpB,UAAU,aAAa,EACvB,WAAW,aAAa,EACxB,cAAc,MAAM,CAAC,YAAY,KAChC,OAAO,CAAC,IAAI,CA8Id,CAAC"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { runHarnessTestFile } from './run.js';
|
|
4
|
+
import { NativeCrashError, StartupStallError, } from './errors.js';
|
|
5
|
+
import { DeviceNotRespondingError } from '@react-native-harness/bridge/server';
|
|
6
|
+
const createRunSummary = () => ({ passed: 0, failed: 0, skipped: 0, todo: 0 });
|
|
7
|
+
const applyJestResultToSummary = (summary, result) => {
|
|
8
|
+
summary.passed += result.numPassingTests;
|
|
9
|
+
summary.failed += result.numFailingTests;
|
|
10
|
+
summary.skipped += result.numPendingTests;
|
|
11
|
+
summary.todo += result.numTodoTests;
|
|
12
|
+
};
|
|
13
|
+
class CancelRun extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'CancelRun';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const buildTestFailure = (err) => {
|
|
20
|
+
if (err instanceof NativeCrashError ||
|
|
21
|
+
err instanceof StartupStallError ||
|
|
22
|
+
err instanceof DeviceNotRespondingError) {
|
|
23
|
+
return { message: err.message, stack: '' };
|
|
24
|
+
}
|
|
25
|
+
return err;
|
|
26
|
+
};
|
|
27
|
+
export const executeRun = async (session, tests, watcher, onStart, onResult, onFailure, globalConfig) => {
|
|
28
|
+
const runId = randomUUID();
|
|
29
|
+
const startTime = Date.now();
|
|
30
|
+
const watchMode = globalConfig.watch || globalConfig.watchAll;
|
|
31
|
+
const rootDir = globalConfig.rootDir ?? process.cwd();
|
|
32
|
+
const testFiles = tests.map((t) => path.relative(rootDir, t.path));
|
|
33
|
+
const summary = createRunSummary();
|
|
34
|
+
const updateRunState = (overrides = {}) => {
|
|
35
|
+
const state = {
|
|
36
|
+
runId,
|
|
37
|
+
startTime,
|
|
38
|
+
testFiles,
|
|
39
|
+
watchMode,
|
|
40
|
+
coverageEnabled: globalConfig.collectCoverage,
|
|
41
|
+
summary,
|
|
42
|
+
status: summary.failed > 0 ? 'failed' : 'passed',
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
session.setRunState(state);
|
|
46
|
+
return state;
|
|
47
|
+
};
|
|
48
|
+
updateRunState();
|
|
49
|
+
await session.callHook('run:started', {
|
|
50
|
+
runId,
|
|
51
|
+
startTime,
|
|
52
|
+
testFiles,
|
|
53
|
+
watchMode,
|
|
54
|
+
coverageEnabled: globalConfig.collectCoverage,
|
|
55
|
+
});
|
|
56
|
+
await session.callHook('metro:initialized', {
|
|
57
|
+
runId,
|
|
58
|
+
port: session.config.metroPort,
|
|
59
|
+
host: session.config.host?.trim() || undefined,
|
|
60
|
+
});
|
|
61
|
+
const shouldResetEnv = session.config.resetEnvironmentBetweenTestFiles;
|
|
62
|
+
let isFirstTest = true;
|
|
63
|
+
let runError;
|
|
64
|
+
try {
|
|
65
|
+
for (const test of tests) {
|
|
66
|
+
if (watcher.isInterrupted())
|
|
67
|
+
throw new CancelRun();
|
|
68
|
+
const relativeTestPath = path.relative(rootDir, test.path);
|
|
69
|
+
const fileStartedAt = Date.now();
|
|
70
|
+
let emittedTestFileFinished = false;
|
|
71
|
+
const emitTestFileFinished = async (options) => {
|
|
72
|
+
emittedTestFileFinished = true;
|
|
73
|
+
await session.callHook('test-file:finished', {
|
|
74
|
+
runId,
|
|
75
|
+
file: relativeTestPath,
|
|
76
|
+
duration: options.duration,
|
|
77
|
+
status: options.status,
|
|
78
|
+
result: options.result,
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
await session.callHook('test-file:started', { runId, file: relativeTestPath });
|
|
82
|
+
try {
|
|
83
|
+
if (shouldResetEnv && !isFirstTest) {
|
|
84
|
+
await session.restartApp(test.path);
|
|
85
|
+
}
|
|
86
|
+
isFirstTest = false;
|
|
87
|
+
session.flushClientLogs();
|
|
88
|
+
await onStart(test);
|
|
89
|
+
await session.ensureAppReady(test.path);
|
|
90
|
+
// Crash detection is handled inside session.runTestFile; NativeCrashError
|
|
91
|
+
// propagates here if a crash wins the race.
|
|
92
|
+
const result = await runHarnessTestFile({
|
|
93
|
+
testPath: test.path,
|
|
94
|
+
session,
|
|
95
|
+
globalConfig,
|
|
96
|
+
projectConfig: test.context.config,
|
|
97
|
+
});
|
|
98
|
+
applyJestResultToSummary(summary, result.jestResult);
|
|
99
|
+
const clientLogs = session.flushClientLogs();
|
|
100
|
+
if (clientLogs.length > 0) {
|
|
101
|
+
result.jestResult.console = clientLogs;
|
|
102
|
+
}
|
|
103
|
+
updateRunState();
|
|
104
|
+
await emitTestFileFinished({
|
|
105
|
+
status: result.harnessResult.status,
|
|
106
|
+
duration: result.duration,
|
|
107
|
+
result: result.harnessResult,
|
|
108
|
+
});
|
|
109
|
+
await onResult(test, result.jestResult);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (!emittedTestFileFinished) {
|
|
113
|
+
await emitTestFileFinished({
|
|
114
|
+
status: 'failed',
|
|
115
|
+
duration: Date.now() - fileStartedAt,
|
|
116
|
+
result: null,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const isRuntimeFailure = err instanceof NativeCrashError ||
|
|
120
|
+
err instanceof StartupStallError ||
|
|
121
|
+
err instanceof DeviceNotRespondingError;
|
|
122
|
+
if (isRuntimeFailure) {
|
|
123
|
+
summary.failed += 1;
|
|
124
|
+
updateRunState();
|
|
125
|
+
}
|
|
126
|
+
if (err instanceof NativeCrashError) {
|
|
127
|
+
session.resetCrashState();
|
|
128
|
+
}
|
|
129
|
+
updateRunState({ error: isRuntimeFailure ? undefined : err });
|
|
130
|
+
onFailure(test, buildTestFailure(err));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
runError = err;
|
|
136
|
+
if (!(err instanceof CancelRun))
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
const runState = updateRunState(runError != null ? { error: runError, status: 'failed' } : {});
|
|
141
|
+
await session.callHook('run:finished', {
|
|
142
|
+
runId,
|
|
143
|
+
startTime,
|
|
144
|
+
duration: Date.now() - startTime,
|
|
145
|
+
testFiles,
|
|
146
|
+
summary,
|
|
147
|
+
status: runState.status ?? (runError != null ? 'failed' : 'passed'),
|
|
148
|
+
...(runError != null ? { error: runError } : {}),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type HarnessContext, type TestExecutionOptions, type TestSuiteResult } from '@react-native-harness/bridge';
|
|
2
|
+
import { type HarnessPlatform } from '@react-native-harness/platforms';
|
|
3
|
+
import { type HarnessPluginManager, type HarnessRunStatus, type HarnessRunSummary } from '@react-native-harness/plugins';
|
|
4
|
+
import { type Config as HarnessConfig } from '@react-native-harness/config';
|
|
5
|
+
import type { Config as JestConfig } from 'jest-runner';
|
|
6
|
+
import { type ClientLogBuffer } from './client-log-handler.js';
|
|
7
|
+
import { type ResourceLockManager } from './resource-lock.js';
|
|
8
|
+
export type HarnessRunState = {
|
|
9
|
+
readonly runId: string;
|
|
10
|
+
readonly startTime: number;
|
|
11
|
+
readonly testFiles: string[];
|
|
12
|
+
readonly watchMode: boolean;
|
|
13
|
+
readonly coverageEnabled: boolean;
|
|
14
|
+
readonly summary?: HarnessRunSummary;
|
|
15
|
+
readonly status?: HarnessRunStatus;
|
|
16
|
+
readonly error?: unknown;
|
|
17
|
+
};
|
|
18
|
+
export type HarnessRunTestsOptions = Exclude<TestExecutionOptions, 'platform'>;
|
|
19
|
+
export type HarnessSession = {
|
|
20
|
+
readonly config: HarnessConfig;
|
|
21
|
+
readonly context: HarnessContext;
|
|
22
|
+
runTestFile: (path: string, options: HarnessRunTestsOptions) => Promise<TestSuiteResult>;
|
|
23
|
+
ensureAppReady: (testFilePath: string) => Promise<void>;
|
|
24
|
+
restartApp: (testFilePath?: string) => Promise<void>;
|
|
25
|
+
resetCrashState: () => void;
|
|
26
|
+
flushClientLogs: () => ClientLogBuffer;
|
|
27
|
+
callHook: HarnessPluginManager<HarnessConfig, HarnessPlatform>['callHook'];
|
|
28
|
+
setRunState: (state: HarnessRunState | null) => void;
|
|
29
|
+
dispose: (reason?: 'normal' | 'abort' | 'error') => Promise<void>;
|
|
30
|
+
};
|
|
31
|
+
export declare const createHarnessSession: (globalConfig: JestConfig.GlobalConfig, { lockManager }?: {
|
|
32
|
+
lockManager?: ResourceLockManager;
|
|
33
|
+
}) => Promise<HarnessSession>;
|
|
34
|
+
//# sourceMappingURL=harness-session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness-session.d.ts","sourceRoot":"","sources":["../src/harness-session.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,cAAc,EAEnB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACrB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,iCAAiC,CAAC;AASzC,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAOvC,OAAO,EAEL,KAAK,MAAM,IAAI,aAAa,EAE7B,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AAMxD,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,oBAAoB,CAAC;AAkB5B,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,iBAAiB,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;AAE/E,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IACzF,cAAc,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,UAAU,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,eAAe,EAAE,MAAM,eAAe,CAAC;IACvC,QAAQ,EAAE,oBAAoB,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC,UAAU,CAAC,CAAC;IAC3E,WAAW,EAAE,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,CAAC;IACrD,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE,CAAC;AAuOF,eAAO,MAAM,oBAAoB,GAC/B,cAAc,UAAU,CAAC,YAAY,EACrC,kBAA8C;IAAE,WAAW,CAAC,EAAE,mBAAmB,CAAA;CAAO,KACvF,OAAO,CAAC,cAAc,CA6YxB,CAAC"}
|