@react-native-harness/jest 1.3.0 → 1.4.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.
Files changed (98) hide show
  1. package/dist/__tests__/bridge.test.js +29 -4
  2. package/dist/__tests__/crash-monitor.test.js +194 -176
  3. package/dist/__tests__/environment-reset.test.d.ts +2 -0
  4. package/dist/__tests__/environment-reset.test.d.ts.map +1 -0
  5. package/dist/__tests__/environment-reset.test.js +238 -0
  6. package/dist/__tests__/errors.test.js +39 -0
  7. package/dist/__tests__/execute-run.test.js +439 -29
  8. package/dist/__tests__/format-harness-error.test.d.ts +2 -0
  9. package/dist/__tests__/format-harness-error.test.d.ts.map +1 -0
  10. package/dist/__tests__/format-harness-error.test.js +27 -0
  11. package/dist/__tests__/harness-cache.test.js +62 -41
  12. package/dist/__tests__/harness-session-signal.test.d.ts +2 -0
  13. package/dist/__tests__/harness-session-signal.test.d.ts.map +1 -0
  14. package/dist/__tests__/harness-session-signal.test.js +298 -0
  15. package/dist/__tests__/harness-session.test.js +217 -234
  16. package/dist/__tests__/index.test.js +62 -11
  17. package/dist/__tests__/metro-port.test.js +2 -0
  18. package/dist/__tests__/run.test.d.ts +2 -0
  19. package/dist/__tests__/run.test.d.ts.map +1 -0
  20. package/dist/__tests__/run.test.js +48 -0
  21. package/dist/__tests__/test-file-platform-filter.test.d.ts +2 -0
  22. package/dist/__tests__/test-file-platform-filter.test.d.ts.map +1 -0
  23. package/dist/__tests__/test-file-platform-filter.test.js +47 -0
  24. package/dist/crash-monitor.d.ts +12 -4
  25. package/dist/crash-monitor.d.ts.map +1 -1
  26. package/dist/crash-monitor.js +135 -102
  27. package/dist/diagnostics/__tests__/derivers.test.d.ts +2 -0
  28. package/dist/diagnostics/__tests__/derivers.test.d.ts.map +1 -0
  29. package/dist/diagnostics/__tests__/derivers.test.js +248 -0
  30. package/dist/diagnostics/__tests__/report.test.d.ts +2 -0
  31. package/dist/diagnostics/__tests__/report.test.d.ts.map +1 -0
  32. package/dist/diagnostics/__tests__/report.test.js +97 -0
  33. package/dist/diagnostics/derivers.d.ts +24 -0
  34. package/dist/diagnostics/derivers.d.ts.map +1 -0
  35. package/dist/diagnostics/derivers.js +146 -0
  36. package/dist/diagnostics/index.d.ts +3 -0
  37. package/dist/diagnostics/index.d.ts.map +1 -0
  38. package/dist/diagnostics/index.js +2 -0
  39. package/dist/diagnostics/report.d.ts +7 -0
  40. package/dist/diagnostics/report.d.ts.map +1 -0
  41. package/dist/diagnostics/report.js +84 -0
  42. package/dist/environment-reset.d.ts +51 -0
  43. package/dist/environment-reset.d.ts.map +1 -0
  44. package/dist/environment-reset.js +83 -0
  45. package/dist/errors.d.ts +10 -0
  46. package/dist/errors.d.ts.map +1 -1
  47. package/dist/errors.js +41 -2
  48. package/dist/execute-run.d.ts +4 -2
  49. package/dist/execute-run.d.ts.map +1 -1
  50. package/dist/execute-run.js +179 -49
  51. package/dist/format-harness-error.d.ts +5 -0
  52. package/dist/format-harness-error.d.ts.map +1 -0
  53. package/dist/format-harness-error.js +43 -0
  54. package/dist/harness-session.d.ts +46 -1
  55. package/dist/harness-session.d.ts.map +1 -1
  56. package/dist/harness-session.js +386 -118
  57. package/dist/index.d.ts +7 -3
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +24 -2
  60. package/dist/run.d.ts.map +1 -1
  61. package/dist/run.js +22 -4
  62. package/dist/test-file-platform-filter.d.ts +5 -0
  63. package/dist/test-file-platform-filter.d.ts.map +1 -0
  64. package/dist/test-file-platform-filter.js +40 -0
  65. package/dist/toTestResult.d.ts +1 -0
  66. package/dist/toTestResult.d.ts.map +1 -1
  67. package/dist/toTestResult.js +2 -2
  68. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  69. package/package.json +8 -7
  70. package/src/__tests__/bridge.test.ts +45 -5
  71. package/src/__tests__/crash-monitor.test.ts +234 -199
  72. package/src/__tests__/environment-reset.test.ts +308 -0
  73. package/src/__tests__/errors.test.ts +75 -0
  74. package/src/__tests__/execute-run.test.ts +561 -37
  75. package/src/__tests__/format-harness-error.test.ts +32 -0
  76. package/src/__tests__/harness-cache.test.ts +91 -0
  77. package/src/__tests__/harness-session.test.ts +303 -0
  78. package/src/__tests__/index.test.ts +92 -12
  79. package/src/__tests__/metro-port.test.ts +2 -0
  80. package/src/__tests__/run.test.ts +74 -0
  81. package/src/__tests__/test-file-platform-filter.test.ts +83 -0
  82. package/src/crash-monitor.ts +220 -131
  83. package/src/diagnostics/__tests__/derivers.test.ts +300 -0
  84. package/src/diagnostics/__tests__/report.test.ts +128 -0
  85. package/src/diagnostics/derivers.ts +170 -0
  86. package/src/diagnostics/index.ts +2 -0
  87. package/src/diagnostics/report.ts +124 -0
  88. package/src/environment-reset.ts +138 -0
  89. package/src/errors.ts +62 -1
  90. package/src/execute-run.ts +230 -57
  91. package/src/format-harness-error.ts +73 -0
  92. package/src/harness-session.ts +535 -162
  93. package/src/index.ts +40 -11
  94. package/src/run.ts +24 -4
  95. package/src/test-file-platform-filter.ts +57 -0
  96. package/src/toTestResult.ts +3 -2
  97. package/tsconfig.json +3 -0
  98. package/tsconfig.lib.json +3 -0
@@ -0,0 +1,51 @@
1
+ import type { Config as HarnessConfig } from '@react-native-harness/config';
2
+ export type ResetStrategyKind = 'process' | 'runtime';
3
+ export type EnvironmentResetStrategy = {
4
+ readonly kind: ResetStrategyKind;
5
+ reset(ctx: {
6
+ testFilePath: string;
7
+ }): Promise<void>;
8
+ };
9
+ /**
10
+ * Normalizes the `resetEnvironmentBetweenTestFiles` config value into a
11
+ * strategy kind, or `null` when no reset should happen between test files
12
+ * (i.e. the value is `false`).
13
+ */
14
+ export declare const resolveResetStrategyKind: (value: HarnessConfig["resetEnvironmentBetweenTestFiles"]) => ResetStrategyKind | null;
15
+ export type ProcessResetStrategyDeps = {
16
+ restart: (testFilePath: string) => Promise<void>;
17
+ };
18
+ /**
19
+ * The 'process' strategy: kill and cold-restart the app process. This is
20
+ * the same implementation `restartApp` uses for crash/timeout recovery.
21
+ */
22
+ export declare const createProcessResetStrategy: (deps: ProcessResetStrategyDeps) => EnvironmentResetStrategy;
23
+ export type RuntimeResetConnection = {
24
+ resetEnvironment: () => Promise<void>;
25
+ };
26
+ export type RuntimeResetStrategyDeps = {
27
+ getConnection: () => RuntimeResetConnection | null;
28
+ expectDisconnect: () => () => void;
29
+ waitForReconnect: (signal: AbortSignal) => Promise<void>;
30
+ isDisconnectError: (error: unknown) => boolean;
31
+ timeoutMs: number;
32
+ };
33
+ /**
34
+ * The 'runtime' strategy: reload the JS runtime in place (DevSettings.reload()
35
+ * on native, window.location.reload() on web) via the resetEnvironment RPC,
36
+ * and wait for the bridge to reconnect. The app process stays alive
37
+ * throughout, so the crash monitor's expected-disconnect window is used to
38
+ * avoid misclassifying the intentional disconnect as a runtime failure.
39
+ */
40
+ export declare const createRuntimeResetStrategy: (deps: RuntimeResetStrategyDeps) => EnvironmentResetStrategy;
41
+ export type EscalationOptions = {
42
+ onEscalate?: (error: unknown) => void;
43
+ };
44
+ /**
45
+ * Wraps a primary strategy so that any failure (no active connection, RPC
46
+ * error, reconnect timeout, ...) falls back to running `fallback` instead of
47
+ * propagating the error. The composite reports the primary's `kind`, since
48
+ * that's the strategy that was requested.
49
+ */
50
+ export declare const withEscalation: (primary: EnvironmentResetStrategy, fallback: EnvironmentResetStrategy, options?: EscalationOptions) => EnvironmentResetStrategy;
51
+ //# sourceMappingURL=environment-reset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment-reset.d.ts","sourceRoot":"","sources":["../src/environment-reset.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAI5E,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,SAAS,CAAC;AAEtD,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IAEjC,KAAK,CAAC,GAAG,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GACnC,OAAO,aAAa,CAAC,kCAAkC,CAAC,KACvD,iBAAiB,GAAG,IAQtB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IAErC,OAAO,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,GACrC,MAAM,wBAAwB,KAC7B,wBAGD,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG;IACnC,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IAErC,aAAa,EAAE,MAAM,sBAAsB,GAAG,IAAI,CAAC;IAGnD,gBAAgB,EAAE,MAAM,MAAM,IAAI,CAAC;IAGnC,gBAAgB,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAIzD,iBAAiB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAE/C,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GACrC,MAAM,wBAAwB,KAC7B,wBAkCD,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG;IAE9B,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GACzB,SAAS,wBAAwB,EACjC,UAAU,wBAAwB,EAClC,UAAS,iBAAsB,KAC9B,wBAUD,CAAC"}
@@ -0,0 +1,83 @@
1
+ import { getTimeoutSignal } from '@react-native-harness/tools';
2
+ const ignorePromiseRejection = () => undefined;
3
+ /**
4
+ * Normalizes the `resetEnvironmentBetweenTestFiles` config value into a
5
+ * strategy kind, or `null` when no reset should happen between test files
6
+ * (i.e. the value is `false`).
7
+ */
8
+ export const resolveResetStrategyKind = (value) => {
9
+ if (value === false) {
10
+ return null;
11
+ }
12
+ if (value === true) {
13
+ return 'process';
14
+ }
15
+ return value;
16
+ };
17
+ /**
18
+ * The 'process' strategy: kill and cold-restart the app process. This is
19
+ * the same implementation `restartApp` uses for crash/timeout recovery.
20
+ */
21
+ export const createProcessResetStrategy = (deps) => ({
22
+ kind: 'process',
23
+ reset: ({ testFilePath }) => deps.restart(testFilePath),
24
+ });
25
+ /**
26
+ * The 'runtime' strategy: reload the JS runtime in place (DevSettings.reload()
27
+ * on native, window.location.reload() on web) via the resetEnvironment RPC,
28
+ * and wait for the bridge to reconnect. The app process stays alive
29
+ * throughout, so the crash monitor's expected-disconnect window is used to
30
+ * avoid misclassifying the intentional disconnect as a runtime failure.
31
+ */
32
+ export const createRuntimeResetStrategy = (deps) => ({
33
+ kind: 'runtime',
34
+ reset: async () => {
35
+ const connection = deps.getConnection();
36
+ if (!connection) {
37
+ throw new Error('No active app connection to reset the runtime on');
38
+ }
39
+ const closeWindow = deps.expectDisconnect();
40
+ try {
41
+ // Start listening for the reconnect before firing the RPC, so a fast
42
+ // reload can't reconnect before we're watching for it.
43
+ const reconnected = deps.waitForReconnect(getTimeoutSignal(deps.timeoutMs));
44
+ // Observe the rejection even when the RPC throws first, so the timeout
45
+ // abort firing later can't become an unhandled rejection. (.catch()
46
+ // returns a new promise; the original is what gets awaited below.)
47
+ reconnected.catch(ignorePromiseRejection);
48
+ try {
49
+ await connection.resetEnvironment();
50
+ }
51
+ catch (error) {
52
+ // The reload can tear the runtime down before the RPC ack flushes.
53
+ // A disconnect-flavored rejection means the reload is in flight, so
54
+ // keep waiting for the reconnect; anything else is a real failure.
55
+ if (!deps.isDisconnectError(error)) {
56
+ throw error;
57
+ }
58
+ }
59
+ await reconnected;
60
+ }
61
+ finally {
62
+ closeWindow();
63
+ }
64
+ },
65
+ });
66
+ /**
67
+ * Wraps a primary strategy so that any failure (no active connection, RPC
68
+ * error, reconnect timeout, ...) falls back to running `fallback` instead of
69
+ * propagating the error. The composite reports the primary's `kind`, since
70
+ * that's the strategy that was requested.
71
+ */
72
+ export const withEscalation = (primary, fallback, options = {}) => ({
73
+ kind: primary.kind,
74
+ reset: async (ctx) => {
75
+ try {
76
+ await primary.reset(ctx);
77
+ }
78
+ catch (error) {
79
+ options.onEscalate?.(error);
80
+ await fallback.reset(ctx);
81
+ }
82
+ },
83
+ });
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;AA6BjF,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;AA+FnB,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,CAsOd,CAAC"}
@@ -1,8 +1,14 @@
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 { resolveResetStrategyKind } from './environment-reset.js';
5
+ import { NativeCrashError, RuntimeDisconnectError, StartupStallError, } from './errors.js';
6
+ import { AppBridgeDisconnectedError, DeviceNotRespondingError, } from '@react-native-harness/bridge/server';
7
+ import { createPlatformSkippedTestResult, shouldRunHarnessTestFile, } from './test-file-platform-filter.js';
8
+ import { formatHarnessErrorMessage } from './format-harness-error.js';
9
+ import { logger } from '@react-native-harness/tools';
10
+ import { printSummary, writeTraceFile } from './diagnostics/index.js';
11
+ const diagnosticsLogger = logger.child('diagnostics');
6
12
  const createRunSummary = () => ({ passed: 0, failed: 0, skipped: 0, todo: 0 });
7
13
  const applyJestResultToSummary = (summary, result) => {
8
14
  summary.passed += result.numPassingTests;
@@ -18,19 +24,71 @@ class CancelRun extends Error {
18
24
  }
19
25
  const buildTestFailure = (err) => {
20
26
  if (err instanceof NativeCrashError ||
27
+ err instanceof RuntimeDisconnectError ||
21
28
  err instanceof StartupStallError ||
29
+ err instanceof AppBridgeDisconnectedError ||
22
30
  err instanceof DeviceNotRespondingError) {
23
31
  return { message: err.message, stack: '' };
24
32
  }
25
33
  return err;
26
34
  };
27
- export const executeRun = async (session, tests, watcher, onStart, onResult, onFailure, globalConfig) => {
35
+ const toJestMode = (declarationMode) => declarationMode;
36
+ const emitHarnessTestStarted = async (emitEvent, event) => {
37
+ await emitEvent('test-case-start', event.file, {
38
+ ancestorTitles: event.ancestorTitles,
39
+ fullName: event.fullName,
40
+ mode: toJestMode(event.declarationMode),
41
+ title: event.name,
42
+ startedAt: event.startedAt,
43
+ });
44
+ };
45
+ const emitHarnessTestFinished = async (emitEvent, event) => {
46
+ const codeFrame = event.error?.codeFrame;
47
+ const failureMessage = formatHarnessErrorMessage(event.error, {
48
+ testStartedAt: event.startedAt,
49
+ });
50
+ const location = codeFrame?.location
51
+ ? { column: codeFrame.location.column, line: codeFrame.location.row }
52
+ : null;
53
+ const testCaseResult = {
54
+ ancestorTitles: event.ancestorTitles,
55
+ duration: event.duration,
56
+ failureDetails: [],
57
+ failureMessages: failureMessage ? [failureMessage] : [],
58
+ fullName: event.fullName,
59
+ location,
60
+ numPassingAsserts: event.status === 'passed' ? 1 : 0,
61
+ startedAt: event.startedAt,
62
+ status: event.status,
63
+ title: event.name,
64
+ };
65
+ await emitEvent('test-case-result', event.file, testCaseResult);
66
+ };
67
+ const isHarnessCaseEvent = (event) => event.type === 'test-started' || event.type === 'test-finished';
68
+ const TIMEOUT_ERROR_NAMES = new Set([
69
+ 'SuiteHookTimeoutError',
70
+ 'TestCaseTimeoutError',
71
+ ]);
72
+ const hasRuntimeTimeout = (result) => (result.error ? TIMEOUT_ERROR_NAMES.has(result.error.name) : false) ||
73
+ result.tests.some((test) => test.error ? TIMEOUT_ERROR_NAMES.has(test.error.name) : false) ||
74
+ result.suites.some(hasRuntimeTimeout);
75
+ export const executeRun = async (session, tests, watcher, emitEvent, globalConfig) => {
28
76
  const runId = randomUUID();
77
+ const diagnostics = session.diagnostics.child({ runId });
78
+ const runSpan = diagnostics.start('run.total', { runId });
29
79
  const startTime = Date.now();
30
80
  const watchMode = globalConfig.watch || globalConfig.watchAll;
31
81
  const rootDir = globalConfig.rootDir ?? process.cwd();
32
82
  const testFiles = tests.map((t) => path.relative(rootDir, t.path));
33
83
  const summary = createRunSummary();
84
+ let caseEventChain = Promise.resolve();
85
+ const unsubscribe = session.onTestRunnerEvent((event) => {
86
+ if (isHarnessCaseEvent(event)) {
87
+ caseEventChain = caseEventChain.then(() => event.type === 'test-started'
88
+ ? emitHarnessTestStarted(emitEvent, event)
89
+ : emitHarnessTestFinished(emitEvent, event));
90
+ }
91
+ });
34
92
  const updateRunState = (overrides = {}) => {
35
93
  const state = {
36
94
  runId,
@@ -38,6 +96,7 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
38
96
  testFiles,
39
97
  watchMode,
40
98
  coverageEnabled: globalConfig.collectCoverage,
99
+ completed: false,
41
100
  summary,
42
101
  status: summary.failed > 0 ? 'failed' : 'passed',
43
102
  ...overrides,
@@ -58,8 +117,11 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
58
117
  port: session.config.metroPort,
59
118
  host: session.config.host?.trim() || undefined,
60
119
  });
61
- const shouldResetEnv = session.config.resetEnvironmentBetweenTestFiles;
120
+ const resetStrategyKind = resolveResetStrategyKind(session.config.resetEnvironmentBetweenTestFiles);
121
+ const platformId = session.context.platform.platformId;
122
+ const knownPlatformIds = new Set(session.config.runners.map((runner) => runner.platformId));
62
123
  let isFirstTest = true;
124
+ let shouldRestartAfterTimeout = false;
63
125
  let runError;
64
126
  try {
65
127
  for (const test of tests) {
@@ -79,55 +141,108 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
79
141
  });
80
142
  };
81
143
  await session.callHook('test-file:started', { runId, file: relativeTestPath });
144
+ const fileSpan = diagnostics.start('run.file', { file: relativeTestPath });
145
+ let fileStatus = 'failed';
82
146
  try {
83
- if (shouldResetEnv && !isFirstTest) {
84
- await session.restartApp(test.path);
147
+ if (!shouldRunHarnessTestFile(test.path, platformId, knownPlatformIds)) {
148
+ try {
149
+ await emitEvent('test-file-start', test);
150
+ const skippedResult = createPlatformSkippedTestResult(test.path);
151
+ applyJestResultToSummary(summary, skippedResult);
152
+ updateRunState();
153
+ fileStatus = 'skipped';
154
+ await emitTestFileFinished({
155
+ status: 'skipped',
156
+ duration: Date.now() - fileStartedAt,
157
+ result: null,
158
+ });
159
+ await emitEvent('test-file-success', test, skippedResult);
160
+ }
161
+ catch (err) {
162
+ fileStatus = 'failed';
163
+ if (!emittedTestFileFinished) {
164
+ await emitTestFileFinished({
165
+ status: 'failed',
166
+ duration: Date.now() - fileStartedAt,
167
+ result: null,
168
+ });
169
+ }
170
+ updateRunState({ error: err });
171
+ await emitEvent('test-file-failure', test, buildTestFailure(err));
172
+ }
173
+ continue;
85
174
  }
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,
175
+ try {
176
+ if (shouldRestartAfterTimeout) {
177
+ // Recovery from a runtime timeout always uses the (stronger)
178
+ // process restart, never the configured reset strategy.
179
+ await session.restartApp(test.path);
180
+ shouldRestartAfterTimeout = false;
181
+ }
182
+ else if (resetStrategyKind !== null && !isFirstTest) {
183
+ await session.resetEnvironment(test.path);
184
+ }
185
+ isFirstTest = false;
186
+ session.flushClientLogs();
187
+ await emitEvent('test-file-start', test);
188
+ await session.ensureAppReady(test.path);
189
+ // Crash detection is handled inside session.runTestFile; NativeCrashError
190
+ // propagates here if a crash wins the race.
191
+ const result = await runHarnessTestFile({
192
+ testPath: test.path,
193
+ session,
194
+ globalConfig,
195
+ projectConfig: test.context.config,
117
196
  });
118
- }
119
- const isRuntimeFailure = err instanceof NativeCrashError ||
120
- err instanceof StartupStallError ||
121
- err instanceof DeviceNotRespondingError;
122
- if (isRuntimeFailure) {
123
- summary.failed += 1;
197
+ applyJestResultToSummary(summary, result.jestResult);
198
+ const clientLogs = session.flushClientLogs();
199
+ if (clientLogs.length > 0) {
200
+ result.jestResult.console = clientLogs;
201
+ }
124
202
  updateRunState();
203
+ fileStatus = result.harnessResult.status;
204
+ await emitTestFileFinished({
205
+ status: result.harnessResult.status,
206
+ duration: result.duration,
207
+ result: result.harnessResult,
208
+ });
209
+ const didRuntimeTimeout = hasRuntimeTimeout(result.harnessResult);
210
+ shouldRestartAfterTimeout = didRuntimeTimeout;
211
+ await caseEventChain;
212
+ await emitEvent('test-file-success', test, result.jestResult);
213
+ if (didRuntimeTimeout) {
214
+ await session.restartApp(test.path);
215
+ shouldRestartAfterTimeout = false;
216
+ }
125
217
  }
126
- if (err instanceof NativeCrashError) {
127
- session.resetCrashState();
218
+ catch (err) {
219
+ fileStatus = 'failed';
220
+ if (!emittedTestFileFinished) {
221
+ await emitTestFileFinished({
222
+ status: 'failed',
223
+ duration: Date.now() - fileStartedAt,
224
+ result: null,
225
+ });
226
+ }
227
+ const isRuntimeFailure = err instanceof NativeCrashError ||
228
+ err instanceof RuntimeDisconnectError ||
229
+ err instanceof StartupStallError ||
230
+ err instanceof AppBridgeDisconnectedError ||
231
+ err instanceof DeviceNotRespondingError;
232
+ if (isRuntimeFailure) {
233
+ summary.failed += 1;
234
+ updateRunState();
235
+ }
236
+ if (err instanceof NativeCrashError || err instanceof RuntimeDisconnectError) {
237
+ session.resetCrashState();
238
+ }
239
+ updateRunState({ error: isRuntimeFailure ? undefined : err });
240
+ await caseEventChain;
241
+ await emitEvent('test-file-failure', test, buildTestFailure(err));
128
242
  }
129
- updateRunState({ error: isRuntimeFailure ? undefined : err });
130
- onFailure(test, buildTestFailure(err));
243
+ }
244
+ finally {
245
+ fileSpan.end({ status: fileStatus });
131
246
  }
132
247
  }
133
248
  }
@@ -137,15 +252,30 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
137
252
  throw err;
138
253
  }
139
254
  finally {
140
- const runState = updateRunState(runError != null ? { error: runError, status: 'failed' } : {});
255
+ const runState = updateRunState(runError != null ? { completed: true, error: runError, status: 'failed' } : { completed: true });
256
+ const finalStatus = runState.status ?? (runError != null ? 'failed' : 'passed');
141
257
  await session.callHook('run:finished', {
142
258
  runId,
143
259
  startTime,
144
260
  duration: Date.now() - startTime,
145
261
  testFiles,
146
262
  summary,
147
- status: runState.status ?? (runError != null ? 'failed' : 'passed'),
263
+ status: finalStatus,
148
264
  ...(runError != null ? { error: runError } : {}),
149
265
  });
266
+ await caseEventChain;
267
+ unsubscribe();
268
+ runSpan.end({ runId, status: finalStatus });
269
+ if (session.diagnostics.enabled) {
270
+ const entries = session.diagnostics.flush();
271
+ try {
272
+ printSummary(entries, (line) => diagnosticsLogger.info(line));
273
+ const tracePath = await writeTraceFile(entries, { projectRoot: rootDir, runId });
274
+ diagnosticsLogger.info(`wrote diagnostics trace to ${tracePath}`);
275
+ }
276
+ catch (reportError) {
277
+ diagnosticsLogger.warn('failed to write diagnostics report: %s', reportError);
278
+ }
279
+ }
150
280
  }
151
281
  };
@@ -0,0 +1,5 @@
1
+ import type { SerializedError } from '@react-native-harness/bridge';
2
+ export declare const formatHarnessErrorMessage: (error: SerializedError | undefined, options?: {
3
+ testStartedAt?: number;
4
+ }) => string | undefined;
5
+ //# sourceMappingURL=format-harness-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-harness-error.d.ts","sourceRoot":"","sources":["../src/format-harness-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AA6CpE,eAAO,MAAM,yBAAyB,GACpC,OAAO,eAAe,GAAG,SAAS,EAClC,UAAS;IACP,aAAa,CAAC,EAAE,MAAM,CAAC;CACnB,KACL,MAAM,GAAG,SAsBX,CAAC"}
@@ -0,0 +1,43 @@
1
+ const formatPendingPromiseStack = (stack) => stack
2
+ .split('\n')
3
+ .map((line) => ` ${line}`)
4
+ .join('\n');
5
+ const formatPendingPromises = (pendingPromises, testStartedAt) => {
6
+ if (pendingPromises.total === 0) {
7
+ return null;
8
+ }
9
+ const lines = [`Pending promises at timeout: ${pendingPromises.total}`];
10
+ if (pendingPromises.items.length < pendingPromises.total) {
11
+ lines.push(`Showing ${pendingPromises.items.length} of ${pendingPromises.total} pending promises.`);
12
+ }
13
+ for (const promise of pendingPromises.items) {
14
+ const age = testStartedAt !== undefined
15
+ ? `, created ${Math.max(0, promise.createdAt - testStartedAt)}ms after test start`
16
+ : '';
17
+ lines.push('');
18
+ lines.push(`Promise #${promise.id}${age}:`);
19
+ if (promise.stack) {
20
+ lines.push(formatPendingPromiseStack(promise.stack));
21
+ }
22
+ else {
23
+ lines.push(' <stack unavailable>');
24
+ }
25
+ }
26
+ return lines.join('\n');
27
+ };
28
+ export const formatHarnessErrorMessage = (error, options = {}) => {
29
+ if (!error) {
30
+ return undefined;
31
+ }
32
+ const parts = [error.message];
33
+ const pendingPromiseDetails = error.diagnostics?.pendingPromises
34
+ ? formatPendingPromises(error.diagnostics.pendingPromises, options.testStartedAt)
35
+ : null;
36
+ if (pendingPromiseDetails) {
37
+ parts.push(pendingPromiseDetails);
38
+ }
39
+ if (error.codeFrame) {
40
+ parts.push(error.codeFrame.content);
41
+ }
42
+ return parts.join('\n\n');
43
+ };