@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.
Files changed (54) hide show
  1. package/dist/__tests__/bridge.test.js +23 -2
  2. package/dist/__tests__/crash-monitor.test.js +146 -192
  3. package/dist/__tests__/errors.test.js +39 -0
  4. package/dist/__tests__/execute-run.test.js +190 -26
  5. package/dist/__tests__/fixtures/mock-platform-runner.d.ts +7 -0
  6. package/dist/__tests__/fixtures/mock-platform-runner.d.ts.map +1 -0
  7. package/dist/__tests__/fixtures/mock-platform-runner.js +6 -0
  8. package/dist/__tests__/harness-session.test.d.ts +2 -0
  9. package/dist/__tests__/harness-session.test.d.ts.map +1 -0
  10. package/dist/__tests__/harness-session.test.js +74 -0
  11. package/dist/__tests__/index.test.js +59 -10
  12. package/dist/__tests__/test-file-platform-filter.test.d.ts +2 -0
  13. package/dist/__tests__/test-file-platform-filter.test.d.ts.map +1 -0
  14. package/dist/__tests__/test-file-platform-filter.test.js +47 -0
  15. package/dist/crash-monitor.d.ts +5 -4
  16. package/dist/crash-monitor.d.ts.map +1 -1
  17. package/dist/crash-monitor.js +116 -102
  18. package/dist/errors.d.ts +10 -0
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/errors.js +41 -2
  21. package/dist/execute-run.d.ts +4 -2
  22. package/dist/execute-run.d.ts.map +1 -1
  23. package/dist/execute-run.js +85 -7
  24. package/dist/harness-session.d.ts +21 -1
  25. package/dist/harness-session.d.ts.map +1 -1
  26. package/dist/harness-session.js +134 -39
  27. package/dist/index.d.ts +7 -3
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +24 -2
  30. package/dist/run.d.ts.map +1 -1
  31. package/dist/run.js +1 -0
  32. package/dist/test-file-platform-filter.d.ts +5 -0
  33. package/dist/test-file-platform-filter.d.ts.map +1 -0
  34. package/dist/test-file-platform-filter.js +40 -0
  35. package/dist/toTestResult.d.ts +1 -0
  36. package/dist/toTestResult.d.ts.map +1 -1
  37. package/dist/toTestResult.js +2 -2
  38. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  39. package/package.json +7 -7
  40. package/src/__tests__/bridge.test.ts +37 -2
  41. package/src/__tests__/crash-monitor.test.ts +182 -231
  42. package/src/__tests__/errors.test.ts +75 -0
  43. package/src/__tests__/execute-run.test.ts +235 -34
  44. package/src/__tests__/harness-session.test.ts +93 -0
  45. package/src/__tests__/index.test.ts +89 -11
  46. package/src/__tests__/test-file-platform-filter.test.ts +83 -0
  47. package/src/crash-monitor.ts +192 -131
  48. package/src/errors.ts +62 -1
  49. package/src/execute-run.ts +125 -11
  50. package/src/harness-session.ts +177 -42
  51. package/src/index.ts +40 -11
  52. package/src/run.ts +1 -0
  53. package/src/test-file-platform-filter.ts +57 -0
  54. package/src/toTestResult.ts +3 -2
@@ -1,136 +1,143 @@
1
- import { NativeCrashError, } from './errors.js';
1
+ import { NativeCrashError, RuntimeDisconnectError, } from './errors.js';
2
2
  import { logger } from '@react-native-harness/tools';
3
3
  const crashLogger = logger.child('crash');
4
+ const CRASH_CLASSIFICATION_SETTLE_MS = 1500;
5
+ const CRASH_LOG_WINDOW_MS = 3000;
4
6
  export class CrashWatchCancelledError extends Error {
5
7
  constructor() {
6
8
  super('Crash watch was cancelled');
7
9
  this.name = 'CrashWatchCancelledError';
8
10
  }
9
11
  }
10
- const mergeCrashDetails = (phase, initial, enriched, fallbackSummary) => ({
12
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
+ const isCrashIndicator = (line) => /uncaught exception|terminating app due to|fatal error|EXC_[A-Z_]+|termination reason|crash|abort/i.test(line) || /\bSIG[A-Z]{2,}\b/.test(line);
14
+ const getMatchingCrashLines = (logs, occurredAt) => logs
15
+ .filter((log) => Math.abs(log.occurredAt - occurredAt) <= CRASH_LOG_WINDOW_MS)
16
+ .map((log) => log.line)
17
+ .filter(isCrashIndicator);
18
+ const buildNativeCrashDetails = (phase, rawLines, summary) => ({
11
19
  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,
20
+ source: rawLines.length > 0 ? 'logs' : 'bridge',
21
+ summary: rawLines.length > 0 ? rawLines.join('\n') : summary,
22
+ rawLines: rawLines.length > 0 ? rawLines : undefined,
22
23
  });
23
- export const createCrashMonitor = ({ appMonitor, platformRunner, }) => {
24
+ const getStatePid = (state) => {
25
+ if (state && 'pid' in state) {
26
+ return state.pid;
27
+ }
28
+ return undefined;
29
+ };
30
+ const mergeCrashDetails = (fallback, extracted) => {
31
+ if (!extracted) {
32
+ return fallback;
33
+ }
34
+ return {
35
+ ...fallback,
36
+ ...extracted,
37
+ phase: fallback.phase,
38
+ source: extracted.source ?? fallback.source,
39
+ summary: extracted.summary ?? fallback.summary,
40
+ rawLines: extracted.rawLines ?? fallback.rawLines,
41
+ };
42
+ };
43
+ const buildRuntimeDisconnectDetails = (phase, rawLines) => ({
44
+ phase,
45
+ source: 'bridge',
46
+ summary: 'The runtime bridge disconnected, but the app session still appears to be running.',
47
+ rawLines: rawLines.length > 0 ? rawLines : undefined,
48
+ });
49
+ export const createCrashMonitor = ({ appSession: initialAppSession = null, } = {}) => {
24
50
  let alive = false;
25
51
  let monitoring = true;
26
52
  let isResolvingCrash = false;
27
53
  let disposed = false;
28
- // Both updated when watch() is called so crashes are attributed to the
29
- // correct test file and lifecycle phase.
54
+ let appSession = null;
55
+ let pendingTimer = null;
30
56
  let currentTestFilePath = '';
31
57
  let currentPhase = 'startup';
32
58
  const watchers = new Set();
33
- const getCrashDetailsProvider = () => {
34
- if ('getCrashDetails' in appMonitor) {
35
- return appMonitor;
36
- }
37
- if (platformRunner.getCrashDetails) {
38
- return platformRunner;
39
- }
40
- return null;
41
- };
42
- const notifyCrash = (err) => {
59
+ const notifyFailure = (err) => {
43
60
  const pending = [...watchers];
44
61
  watchers.clear();
45
62
  for (const fn of pending)
46
63
  fn(err);
47
64
  };
48
- const handleCrash = async (phase, details, fallbackSummary) => {
49
- if (isResolvingCrash)
65
+ const clearPendingTimer = () => {
66
+ if (pendingTimer) {
67
+ clearTimeout(pendingTimer);
68
+ pendingTimer = null;
69
+ }
70
+ };
71
+ const classify = async (pending, trigger) => {
72
+ if (disposed || !monitoring || isResolvingCrash) {
50
73
  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
74
  }
75
+ isResolvingCrash = true;
57
76
  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));
77
+ const session = appSession;
78
+ const state = await session?.getState();
79
+ const logs = session?.getLogs() ?? [];
80
+ const rawLines = getMatchingCrashLines(logs, pending.occurredAt);
81
+ if (state?.status === 'running' && trigger === 'bridge-disconnect') {
82
+ crashLogger.debug('runtime bridge disconnected without confirmed app death');
83
+ notifyFailure(new RuntimeDisconnectError(pending.testFilePath, buildRuntimeDisconnectDetails(pending.phase, rawLines)));
84
+ return;
85
+ }
86
+ alive = false;
87
+ crashLogger.debug('native crash detected (phase=%s)', pending.phase);
88
+ for (const line of rawLines) {
89
+ crashLogger.debug('%s', line);
90
+ }
91
+ const fallbackSummary = trigger === 'bridge-disconnect'
92
+ ? 'The app process exited after the runtime bridge disconnected, but no crash log lines were found.'
93
+ : 'The app process exited, but no crash log lines were found.';
94
+ const details = buildNativeCrashDetails(pending.phase, rawLines, fallbackSummary);
95
+ const extractedDetails = session?.getCrashDetails
96
+ ? await session
97
+ .getCrashDetails({
98
+ occurredAt: pending.occurredAt,
99
+ pid: getStatePid(state),
100
+ processName: details.processName,
101
+ testFilePath: pending.testFilePath,
102
+ })
103
+ .catch((error) => {
104
+ crashLogger.warn('failed to extract native crash details: %s', error);
105
+ return null;
106
+ })
107
+ : null;
108
+ notifyFailure(new NativeCrashError(pending.testFilePath, mergeCrashDetails(details, extractedDetails)));
74
109
  }
75
110
  finally {
76
111
  isResolvingCrash = false;
112
+ pendingTimer = null;
77
113
  }
78
114
  };
79
- const confirmAndHandleCrash = async (phase, details, fallbackSummary) => {
80
- if (disposed || !monitoring)
115
+ const startCrashResolution = (trigger) => {
116
+ if (disposed || !monitoring || isResolvingCrash) {
81
117
  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
118
  }
119
+ clearPendingTimer();
120
+ const pending = {
121
+ testFilePath: currentTestFilePath,
122
+ phase: currentPhase,
123
+ occurredAt: Date.now(),
124
+ };
125
+ pendingTimer = setTimeout(() => {
126
+ void classify(pending, trigger);
127
+ }, trigger === 'bridge-disconnect' ? CRASH_CLASSIFICATION_SETTLE_MS : 0);
91
128
  };
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
- }
129
+ const appSessionListener = (event) => {
112
130
  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
+ startCrashResolution('app-exit');
131
132
  }
132
133
  };
133
- appMonitor.addListener(appMonitorListener);
134
+ const setAppSession = (session) => {
135
+ appSession?.removeListener(appSessionListener);
136
+ appSession = session;
137
+ alive = Boolean(session);
138
+ session?.addListener(appSessionListener);
139
+ };
140
+ setAppSession(initialAppSession);
134
141
  const watch = (testFilePath, phase) => {
135
142
  currentTestFilePath = testFilePath;
136
143
  currentPhase = phase;
@@ -152,25 +159,32 @@ export const createCrashMonitor = ({ appMonitor, platformRunner, }) => {
152
159
  isAlive: () => alive,
153
160
  stop: async () => {
154
161
  monitoring = false;
155
- await appMonitor.stop();
162
+ clearPendingTimer();
163
+ await sleep(0);
156
164
  },
157
165
  start: async () => {
158
166
  monitoring = true;
159
- await appMonitor.start();
160
167
  },
161
168
  reset: () => {
162
- alive = false;
169
+ alive = Boolean(appSession);
163
170
  watchers.clear();
164
171
  isResolvingCrash = false;
165
172
  currentTestFilePath = '';
173
+ clearPendingTimer();
174
+ },
175
+ setAppSession,
176
+ handleBridgeDisconnect: () => {
177
+ startCrashResolution('bridge-disconnect');
166
178
  },
167
179
  dispose: async () => {
168
180
  disposed = true;
169
181
  monitoring = false;
170
182
  watchers.clear();
171
183
  isResolvingCrash = false;
172
- appMonitor.removeListener(appMonitorListener);
173
- await appMonitor.dispose();
184
+ clearPendingTimer();
185
+ appSession?.removeListener(appSessionListener);
186
+ appSession = null;
187
+ alive = false;
174
188
  },
175
189
  };
176
190
  };
package/dist/errors.d.ts CHANGED
@@ -24,6 +24,9 @@ export type NativeCrashPhase = 'startup' | 'execution';
24
24
  export type NativeCrashDetails = AppCrashDetails & {
25
25
  phase: NativeCrashPhase;
26
26
  };
27
+ export type RuntimeDisconnectDetails = AppCrashDetails & {
28
+ phase: NativeCrashPhase;
29
+ };
27
30
  export declare class NativeCrashError extends HarnessError {
28
31
  readonly testFilePath: string;
29
32
  readonly details: NativeCrashDetails;
@@ -31,4 +34,11 @@ export declare class NativeCrashError extends HarnessError {
31
34
  constructor(testFilePath: string, details: NativeCrashDetails, lastKnownTest?: string | undefined);
32
35
  get phase(): NativeCrashPhase;
33
36
  }
37
+ export declare class RuntimeDisconnectError extends HarnessError {
38
+ readonly testFilePath: string;
39
+ readonly details: RuntimeDisconnectDetails;
40
+ constructor(testFilePath: string, details: RuntimeDisconnectDetails);
41
+ get phase(): NativeCrashPhase;
42
+ }
43
+ export type HarnessRuntimeFailure = NativeCrashError | RuntimeDisconnectError;
34
44
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EACL,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,qCAAqC,CAAC;AAE7C,qBAAa,sBAAuB,SAAQ,YAAY;;CAKvD;AAED,qBAAa,mBAAoB,SAAQ,YAAY;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,0BAA2B,SAAQ,YAAY;;CAK3D;AAED,qBAAa,yBAA0B,SAAQ,YAAY;aAC7B,OAAO,EAAE,MAAM;gBAAf,OAAO,EAAE,MAAM;CAM5C;AAED,qBAAa,4BAA6B,SAAQ,YAAY;aAE1C,WAAW,EAAE,MAAM;aACnB,QAAQ,EAAE,MAAM;gBADhB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM;CAQnC;AAED,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,WAAW,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAAG,eAAe,GAAG;IACjD,KAAK,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAmDF,qBAAa,gBAAiB,SAAQ,YAAY;aAE9B,YAAY,EAAE,MAAM;aACpB,OAAO,EAAE,kBAAkB;aAC3B,aAAa,CAAC,EAAE,MAAM;gBAFtB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,kBAAkB,EAC3B,aAAa,CAAC,EAAE,MAAM,YAAA;IAOxC,IAAI,KAAK,qBAER;CACF"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,YAAY,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EACL,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,qCAAqC,CAAC;AAE7C,qBAAa,sBAAuB,SAAQ,YAAY;;CAKvD;AAED,qBAAa,mBAAoB,SAAQ,YAAY;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,0BAA2B,SAAQ,YAAY;;CAK3D;AAED,qBAAa,yBAA0B,SAAQ,YAAY;aAC7B,OAAO,EAAE,MAAM;gBAAf,OAAO,EAAE,MAAM;CAM5C;AAED,qBAAa,4BAA6B,SAAQ,YAAY;aAE1C,WAAW,EAAE,MAAM;aACnB,QAAQ,EAAE,MAAM;gBADhB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM;CAQnC;AAED,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,WAAW,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAAG,eAAe,GAAG;IACjD,KAAK,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,eAAe,GAAG;IACvD,KAAK,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAmEF,qBAAa,gBAAiB,SAAQ,YAAY;aAE9B,YAAY,EAAE,MAAM;aACpB,OAAO,EAAE,kBAAkB;aAC3B,aAAa,CAAC,EAAE,MAAM;gBAFtB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,kBAAkB,EAC3B,aAAa,CAAC,EAAE,MAAM,YAAA;IAOxC,IAAI,KAAK,qBAER;CACF;AA0BD,qBAAa,sBAAuB,SAAQ,YAAY;aAEpC,YAAY,EAAE,MAAM;aACpB,OAAO,EAAE,wBAAwB;gBADjC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,wBAAwB;IAOnD,IAAI,KAAK,qBAER;CACF;AAED,MAAM,MAAM,qBAAqB,GAAG,gBAAgB,GAAG,sBAAsB,CAAC"}
package/dist/errors.js CHANGED
@@ -1,4 +1,4 @@
1
- import { HarnessError } from '@react-native-harness/tools';
1
+ import { formatPath, HarnessError } from '@react-native-harness/tools';
2
2
  export { StartupStallError, } from '@react-native-harness/bundler-metro';
3
3
  export class NoRunnerSpecifiedError extends HarnessError {
4
4
  constructor() {
@@ -39,12 +39,21 @@ export class MetroPortRangeExhaustedError extends HarnessError {
39
39
  this.name = 'MetroPortRangeExhaustedError';
40
40
  }
41
41
  }
42
- const buildNativeCrashMessage = ({ phase, summary, signal, exceptionType, processName, pid, stackTrace, artifactType, }) => {
42
+ const buildNativeCrashMessage = ({ phase, summary, signal, exceptionType, processName, pid, stackTrace, artifactType, artifactPath, enrichmentArtifacts, }) => {
43
43
  const lines = [
44
44
  phase === 'startup'
45
45
  ? 'The native app crashed while preparing to run this test file.'
46
46
  : 'The native app crashed during test execution.',
47
47
  ];
48
+ lines.push(artifactPath
49
+ ? `Harness extracted the crash log: ${formatPath(artifactPath)}`
50
+ : "Harness couldn't extract the crash log.");
51
+ if (enrichmentArtifacts && enrichmentArtifacts.length > 0) {
52
+ lines.push('Additional crash artifacts:');
53
+ for (const artifact of enrichmentArtifacts) {
54
+ lines.push(` - ${formatPath(artifact.artifactPath)}`);
55
+ }
56
+ }
48
57
  const hasCrashBlock = summary?.includes('\n') ?? false;
49
58
  const shouldRenderSummary = Boolean(summary) &&
50
59
  !(!hasCrashBlock && artifactType === 'ios-crash-report');
@@ -89,3 +98,33 @@ export class NativeCrashError extends HarnessError {
89
98
  return this.details.phase;
90
99
  }
91
100
  }
101
+ const buildRuntimeDisconnectMessage = ({ phase, summary, rawLines, }) => {
102
+ const lines = [
103
+ phase === 'startup'
104
+ ? 'The native runtime disconnected while preparing to run this test file.'
105
+ : 'The native runtime disconnected during test execution.',
106
+ ];
107
+ if (summary) {
108
+ lines.push('');
109
+ lines.push(summary);
110
+ }
111
+ if (rawLines && rawLines.length > 0 && summary !== rawLines.join('\n')) {
112
+ lines.push('');
113
+ lines.push(...rawLines);
114
+ }
115
+ return lines.join('\n');
116
+ };
117
+ export class RuntimeDisconnectError extends HarnessError {
118
+ testFilePath;
119
+ details;
120
+ constructor(testFilePath, details) {
121
+ super(buildRuntimeDisconnectMessage(details));
122
+ this.testFilePath = testFilePath;
123
+ this.details = details;
124
+ this.name = 'RuntimeDisconnectError';
125
+ this.stack = `${this.name}: ${this.message.split('\n')[0]}`;
126
+ }
127
+ get phase() {
128
+ return this.details.phase;
129
+ }
130
+ }
@@ -1,4 +1,6 @@
1
- import type { OnTestFailure, OnTestStart, OnTestSuccess, Test, TestWatcher, Config } from 'jest-runner';
1
+ import type { Test, TestWatcher, Config, TestEvents } from 'jest-runner';
2
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>;
3
+ type EmitTestEvent = <Name extends keyof TestEvents>(eventName: Name, ...eventData: TestEvents[Name]) => Promise<void>;
4
+ export declare const executeRun: (session: HarnessSession, tests: Array<Test>, watcher: TestWatcher, emitEvent: EmitTestEvent, globalConfig: Config.GlobalConfig) => Promise<void>;
5
+ export {};
4
6
  //# sourceMappingURL=execute-run.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"execute-run.d.ts","sourceRoot":"","sources":["../src/execute-run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,WAAW,EACX,MAAM,EACN,UAAU,EACX,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,KAAK,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AAsBjF,KAAK,aAAa,GAAG,CAAC,IAAI,SAAS,MAAM,UAAU,EACjD,SAAS,EAAE,IAAI,EACf,GAAG,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,KAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;AAmFnB,eAAO,MAAM,UAAU,GACrB,SAAS,cAAc,EACvB,OAAO,KAAK,CAAC,IAAI,CAAC,EAClB,SAAS,WAAW,EACpB,WAAW,aAAa,EACxB,cAAc,MAAM,CAAC,YAAY,KAChC,OAAO,CAAC,IAAI,CA8Ld,CAAC"}
@@ -1,8 +1,9 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import path from 'node:path';
3
3
  import { runHarnessTestFile } from './run.js';
4
- import { NativeCrashError, StartupStallError, } from './errors.js';
5
- import { DeviceNotRespondingError } from '@react-native-harness/bridge/server';
4
+ import { NativeCrashError, RuntimeDisconnectError, StartupStallError, } from './errors.js';
5
+ import { AppBridgeDisconnectedError, DeviceNotRespondingError, } from '@react-native-harness/bridge/server';
6
+ import { createPlatformSkippedTestResult, shouldRunHarnessTestFile, } from './test-file-platform-filter.js';
6
7
  const createRunSummary = () => ({ passed: 0, failed: 0, skipped: 0, todo: 0 });
7
8
  const applyJestResultToSummary = (summary, result) => {
8
9
  summary.passed += result.numPassingTests;
@@ -18,19 +19,62 @@ class CancelRun extends Error {
18
19
  }
19
20
  const buildTestFailure = (err) => {
20
21
  if (err instanceof NativeCrashError ||
22
+ err instanceof RuntimeDisconnectError ||
21
23
  err instanceof StartupStallError ||
24
+ err instanceof AppBridgeDisconnectedError ||
22
25
  err instanceof DeviceNotRespondingError) {
23
26
  return { message: err.message, stack: '' };
24
27
  }
25
28
  return err;
26
29
  };
27
- export const executeRun = async (session, tests, watcher, onStart, onResult, onFailure, globalConfig) => {
30
+ const toJestMode = (declarationMode) => declarationMode;
31
+ const emitHarnessTestStarted = async (emitEvent, event) => {
32
+ await emitEvent('test-case-start', event.file, {
33
+ ancestorTitles: event.ancestorTitles,
34
+ fullName: event.fullName,
35
+ mode: toJestMode(event.declarationMode),
36
+ title: event.name,
37
+ startedAt: event.startedAt,
38
+ });
39
+ };
40
+ const emitHarnessTestFinished = async (emitEvent, event) => {
41
+ const failureMessage = event.error?.message;
42
+ const codeFrame = event.error?.codeFrame;
43
+ const location = codeFrame?.location
44
+ ? { column: codeFrame.location.column, line: codeFrame.location.row }
45
+ : null;
46
+ const testCaseResult = {
47
+ ancestorTitles: event.ancestorTitles,
48
+ duration: event.duration,
49
+ failureDetails: [],
50
+ failureMessages: failureMessage
51
+ ? [`${failureMessage}${codeFrame ? `\n\n${codeFrame.content}` : ''}`]
52
+ : [],
53
+ fullName: event.fullName,
54
+ location,
55
+ numPassingAsserts: event.status === 'passed' ? 1 : 0,
56
+ startedAt: event.startedAt,
57
+ status: event.status,
58
+ title: event.name,
59
+ };
60
+ await emitEvent('test-case-result', event.file, testCaseResult);
61
+ };
62
+ const isHarnessCaseEvent = (event) => event.type === 'test-started' || event.type === 'test-finished';
63
+ export const executeRun = async (session, tests, watcher, emitEvent, globalConfig) => {
28
64
  const runId = randomUUID();
29
65
  const startTime = Date.now();
30
66
  const watchMode = globalConfig.watch || globalConfig.watchAll;
31
67
  const rootDir = globalConfig.rootDir ?? process.cwd();
32
68
  const testFiles = tests.map((t) => path.relative(rootDir, t.path));
33
69
  const summary = createRunSummary();
70
+ let caseEventChain = Promise.resolve();
71
+ const unsubscribe = session.onTestRunnerEvent((event) => {
72
+ if (isHarnessCaseEvent(event)) {
73
+ caseEventChain = caseEventChain.then(() => event.type === 'test-started'
74
+ ? emitHarnessTestStarted(emitEvent, event)
75
+ : emitHarnessTestFinished(emitEvent, event));
76
+ }
77
+ });
34
78
  const updateRunState = (overrides = {}) => {
35
79
  const state = {
36
80
  runId,
@@ -59,6 +103,8 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
59
103
  host: session.config.host?.trim() || undefined,
60
104
  });
61
105
  const shouldResetEnv = session.config.resetEnvironmentBetweenTestFiles;
106
+ const platformId = session.context.platform.platformId;
107
+ const knownPlatformIds = new Set(session.config.runners.map((runner) => runner.platformId));
62
108
  let isFirstTest = true;
63
109
  let runError;
64
110
  try {
@@ -79,13 +125,39 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
79
125
  });
80
126
  };
81
127
  await session.callHook('test-file:started', { runId, file: relativeTestPath });
128
+ if (!shouldRunHarnessTestFile(test.path, platformId, knownPlatformIds)) {
129
+ try {
130
+ await emitEvent('test-file-start', test);
131
+ const skippedResult = createPlatformSkippedTestResult(test.path);
132
+ applyJestResultToSummary(summary, skippedResult);
133
+ updateRunState();
134
+ await emitTestFileFinished({
135
+ status: 'skipped',
136
+ duration: Date.now() - fileStartedAt,
137
+ result: null,
138
+ });
139
+ await emitEvent('test-file-success', test, skippedResult);
140
+ }
141
+ catch (err) {
142
+ if (!emittedTestFileFinished) {
143
+ await emitTestFileFinished({
144
+ status: 'failed',
145
+ duration: Date.now() - fileStartedAt,
146
+ result: null,
147
+ });
148
+ }
149
+ updateRunState({ error: err });
150
+ await emitEvent('test-file-failure', test, buildTestFailure(err));
151
+ }
152
+ continue;
153
+ }
82
154
  try {
83
155
  if (shouldResetEnv && !isFirstTest) {
84
156
  await session.restartApp(test.path);
85
157
  }
86
158
  isFirstTest = false;
87
159
  session.flushClientLogs();
88
- await onStart(test);
160
+ await emitEvent('test-file-start', test);
89
161
  await session.ensureAppReady(test.path);
90
162
  // Crash detection is handled inside session.runTestFile; NativeCrashError
91
163
  // propagates here if a crash wins the race.
@@ -106,7 +178,8 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
106
178
  duration: result.duration,
107
179
  result: result.harnessResult,
108
180
  });
109
- await onResult(test, result.jestResult);
181
+ await caseEventChain;
182
+ await emitEvent('test-file-success', test, result.jestResult);
110
183
  }
111
184
  catch (err) {
112
185
  if (!emittedTestFileFinished) {
@@ -117,17 +190,20 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
117
190
  });
118
191
  }
119
192
  const isRuntimeFailure = err instanceof NativeCrashError ||
193
+ err instanceof RuntimeDisconnectError ||
120
194
  err instanceof StartupStallError ||
195
+ err instanceof AppBridgeDisconnectedError ||
121
196
  err instanceof DeviceNotRespondingError;
122
197
  if (isRuntimeFailure) {
123
198
  summary.failed += 1;
124
199
  updateRunState();
125
200
  }
126
- if (err instanceof NativeCrashError) {
201
+ if (err instanceof NativeCrashError || err instanceof RuntimeDisconnectError) {
127
202
  session.resetCrashState();
128
203
  }
129
204
  updateRunState({ error: isRuntimeFailure ? undefined : err });
130
- onFailure(test, buildTestFailure(err));
205
+ await caseEventChain;
206
+ await emitEvent('test-file-failure', test, buildTestFailure(err));
131
207
  }
132
208
  }
133
209
  }
@@ -147,5 +223,7 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
147
223
  status: runState.status ?? (runError != null ? 'failed' : 'passed'),
148
224
  ...(runError != null ? { error: runError } : {}),
149
225
  });
226
+ await caseEventChain;
227
+ unsubscribe();
150
228
  }
151
229
  };
@@ -1,10 +1,22 @@
1
- import { type HarnessContext, type TestExecutionOptions, type TestSuiteResult } from '@react-native-harness/bridge';
1
+ import { type AppConnection } from '@react-native-harness/bridge/server';
2
+ import { type HarnessContext, type TestRunnerEvents, type TestExecutionOptions, type TestSuiteResult } from '@react-native-harness/bridge';
2
3
  import { type HarnessPlatform } from '@react-native-harness/platforms';
3
4
  import { type HarnessPluginManager, type HarnessRunStatus, type HarnessRunSummary } from '@react-native-harness/plugins';
4
5
  import { type Config as HarnessConfig } from '@react-native-harness/config';
5
6
  import type { Config as JestConfig } from 'jest-runner';
7
+ import { type CrashMonitor } from './crash-monitor.js';
6
8
  import { type ClientLogBuffer } from './client-log-handler.js';
7
9
  import { type ResourceLockManager } from './resource-lock.js';
10
+ type DisconnectObservableBridge = {
11
+ readonly connection: AppConnection | null;
12
+ on: (event: 'disconnected', listener: () => void) => void;
13
+ off: (event: 'disconnected', listener: () => void) => void;
14
+ };
15
+ export declare const waitForBridgeDisconnectOrTimeout: ({ bridge, connection, timeoutMs, }: {
16
+ bridge: DisconnectObservableBridge;
17
+ connection: AppConnection;
18
+ timeoutMs: number;
19
+ }) => Promise<boolean>;
8
20
  export type HarnessRunState = {
9
21
  readonly runId: string;
10
22
  readonly startTime: number;
@@ -19,6 +31,7 @@ export type HarnessRunTestsOptions = Exclude<TestExecutionOptions, 'platform'>;
19
31
  export type HarnessSession = {
20
32
  readonly config: HarnessConfig;
21
33
  readonly context: HarnessContext;
34
+ onTestRunnerEvent: (listener: (event: TestRunnerEvents) => void) => () => void;
22
35
  runTestFile: (path: string, options: HarnessRunTestsOptions) => Promise<TestSuiteResult>;
23
36
  ensureAppReady: (testFilePath: string) => Promise<void>;
24
37
  restartApp: (testFilePath?: string) => Promise<void>;
@@ -28,7 +41,14 @@ export type HarnessSession = {
28
41
  setRunState: (state: HarnessRunState | null) => void;
29
42
  dispose: (reason?: 'normal' | 'abort' | 'error') => Promise<void>;
30
43
  };
44
+ export declare const waitForStartupCrash: ({ crashMonitor, detectNativeCrashes, testFilePath, signal, }: {
45
+ crashMonitor: CrashMonitor;
46
+ detectNativeCrashes: boolean;
47
+ testFilePath: string;
48
+ signal: AbortSignal;
49
+ }) => Promise<never>;
31
50
  export declare const createHarnessSession: (globalConfig: JestConfig.GlobalConfig, { lockManager }?: {
32
51
  lockManager?: ResourceLockManager;
33
52
  }) => Promise<HarnessSession>;
53
+ export {};
34
54
  //# sourceMappingURL=harness-session.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"harness-session.d.ts","sourceRoot":"","sources":["../src/harness-session.ts"],"names":[],"mappings":"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"}