@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,308 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ createProcessResetStrategy,
4
+ createRuntimeResetStrategy,
5
+ resolveResetStrategyKind,
6
+ withEscalation,
7
+ type EnvironmentResetStrategy,
8
+ } from '../environment-reset.js';
9
+
10
+ describe('resolveResetStrategyKind', () => {
11
+ it('normalizes true to the process strategy', () => {
12
+ expect(resolveResetStrategyKind(true)).toBe('process');
13
+ });
14
+
15
+ it('normalizes false to null (no reset)', () => {
16
+ expect(resolveResetStrategyKind(false)).toBeNull();
17
+ });
18
+
19
+ it('passes through the process string value', () => {
20
+ expect(resolveResetStrategyKind('process')).toBe('process');
21
+ });
22
+
23
+ it('passes through the runtime string value', () => {
24
+ expect(resolveResetStrategyKind('runtime')).toBe('runtime');
25
+ });
26
+ });
27
+
28
+ describe('createProcessResetStrategy', () => {
29
+ it('has kind "process" and delegates to the injected restart function', async () => {
30
+ const restart = vi.fn(async () => undefined);
31
+ const strategy = createProcessResetStrategy({ restart });
32
+
33
+ expect(strategy.kind).toBe('process');
34
+ await strategy.reset({ testFilePath: '/tests/example.harness.ts' });
35
+
36
+ expect(restart).toHaveBeenCalledWith('/tests/example.harness.ts');
37
+ });
38
+
39
+ it('propagates restart failures', async () => {
40
+ const restart = vi.fn(async () => {
41
+ throw new Error('restart failed');
42
+ });
43
+ const strategy = createProcessResetStrategy({ restart });
44
+
45
+ await expect(
46
+ strategy.reset({ testFilePath: '/tests/example.harness.ts' })
47
+ ).rejects.toThrow('restart failed');
48
+ });
49
+ });
50
+
51
+ describe('createRuntimeResetStrategy', () => {
52
+ const testFilePath = '/tests/example.harness.ts';
53
+
54
+ it('has kind "runtime" and resets via the connection then waits for reconnect', async () => {
55
+ const callOrder: string[] = [];
56
+ const closeWindow = vi.fn();
57
+ const connection = {
58
+ resetEnvironment: vi.fn(async () => {
59
+ callOrder.push('resetEnvironment');
60
+ }),
61
+ };
62
+ const waitForReconnect = vi.fn(async () => {
63
+ callOrder.push('waitForReconnect-started');
64
+ });
65
+
66
+ const strategy = createRuntimeResetStrategy({
67
+ getConnection: () => connection,
68
+ expectDisconnect: () => {
69
+ callOrder.push('expectDisconnect');
70
+ return closeWindow;
71
+ },
72
+ waitForReconnect,
73
+ isDisconnectError: () => false,
74
+ timeoutMs: 1000,
75
+ });
76
+
77
+ expect(strategy.kind).toBe('runtime');
78
+ await strategy.reset({ testFilePath });
79
+
80
+ expect(callOrder).toEqual([
81
+ 'expectDisconnect',
82
+ 'waitForReconnect-started',
83
+ 'resetEnvironment',
84
+ ]);
85
+ expect(waitForReconnect).toHaveBeenCalledWith(expect.any(AbortSignal));
86
+ expect(closeWindow).toHaveBeenCalledOnce();
87
+ });
88
+
89
+ it('starts waiting for reconnect before firing the RPC', async () => {
90
+ const callOrder: string[] = [];
91
+ const connection = {
92
+ resetEnvironment: vi.fn(async () => {
93
+ callOrder.push('resetEnvironment');
94
+ }),
95
+ };
96
+ const waitForReconnect = vi.fn(() => {
97
+ callOrder.push('waitForReconnect-called');
98
+ return Promise.resolve();
99
+ });
100
+
101
+ const strategy = createRuntimeResetStrategy({
102
+ getConnection: () => connection,
103
+ expectDisconnect: () => () => undefined,
104
+ waitForReconnect,
105
+ isDisconnectError: () => false,
106
+ timeoutMs: 1000,
107
+ });
108
+
109
+ await strategy.reset({ testFilePath });
110
+
111
+ expect(callOrder).toEqual(['waitForReconnect-called', 'resetEnvironment']);
112
+ });
113
+
114
+ it('throws when there is no active app connection', async () => {
115
+ const expectDisconnect = vi.fn();
116
+ const strategy = createRuntimeResetStrategy({
117
+ getConnection: () => null,
118
+ expectDisconnect,
119
+ waitForReconnect: vi.fn(async () => undefined),
120
+ isDisconnectError: () => false,
121
+ timeoutMs: 1000,
122
+ });
123
+
124
+ await expect(strategy.reset({ testFilePath })).rejects.toThrow(
125
+ 'No active app connection'
126
+ );
127
+ expect(expectDisconnect).not.toHaveBeenCalled();
128
+ });
129
+
130
+ it('closes the expected-disconnect window even when the RPC call fails', async () => {
131
+ const closeWindow = vi.fn();
132
+ const connection = {
133
+ resetEnvironment: vi.fn(async () => {
134
+ throw new Error('rpc failed');
135
+ }),
136
+ };
137
+
138
+ const strategy = createRuntimeResetStrategy({
139
+ getConnection: () => connection,
140
+ expectDisconnect: () => closeWindow,
141
+ waitForReconnect: vi.fn(async () => undefined),
142
+ isDisconnectError: () => false,
143
+ timeoutMs: 1000,
144
+ });
145
+
146
+ await expect(strategy.reset({ testFilePath })).rejects.toThrow('rpc failed');
147
+ expect(closeWindow).toHaveBeenCalledOnce();
148
+ });
149
+
150
+ it('closes the expected-disconnect window even when reconnect times out', async () => {
151
+ const closeWindow = vi.fn();
152
+ const connection = { resetEnvironment: vi.fn(async () => undefined) };
153
+
154
+ const strategy = createRuntimeResetStrategy({
155
+ getConnection: () => connection,
156
+ expectDisconnect: () => closeWindow,
157
+ waitForReconnect: vi.fn(async () => {
158
+ throw new Error('reconnect timed out');
159
+ }),
160
+ isDisconnectError: () => false,
161
+ timeoutMs: 1000,
162
+ });
163
+
164
+ await expect(strategy.reset({ testFilePath })).rejects.toThrow(
165
+ 'reconnect timed out'
166
+ );
167
+ expect(closeWindow).toHaveBeenCalledOnce();
168
+ });
169
+
170
+ it('treats a disconnect-flavored RPC rejection as reload-in-flight and succeeds', async () => {
171
+ const disconnectError = new Error('app bridge disconnected');
172
+ const connection = {
173
+ resetEnvironment: vi.fn(async () => {
174
+ throw disconnectError;
175
+ }),
176
+ };
177
+ const waitForReconnect = vi.fn(async () => undefined);
178
+
179
+ const strategy = createRuntimeResetStrategy({
180
+ getConnection: () => connection,
181
+ expectDisconnect: () => () => undefined,
182
+ waitForReconnect,
183
+ isDisconnectError: (error) => error === disconnectError,
184
+ timeoutMs: 1000,
185
+ });
186
+
187
+ await expect(strategy.reset({ testFilePath })).resolves.toBeUndefined();
188
+ expect(waitForReconnect).toHaveBeenCalledOnce();
189
+ });
190
+
191
+ it('still throws on a non-disconnect RPC rejection', async () => {
192
+ const connection = {
193
+ resetEnvironment: vi.fn(async () => {
194
+ throw new Error('rpc failed');
195
+ }),
196
+ };
197
+
198
+ const strategy = createRuntimeResetStrategy({
199
+ getConnection: () => connection,
200
+ expectDisconnect: () => () => undefined,
201
+ waitForReconnect: vi.fn(async () => undefined),
202
+ isDisconnectError: (error) =>
203
+ error instanceof Error && error.message === 'app bridge disconnected',
204
+ timeoutMs: 1000,
205
+ });
206
+
207
+ await expect(strategy.reset({ testFilePath })).rejects.toThrow('rpc failed');
208
+ });
209
+
210
+ it('does not leave an unhandled rejection when the RPC fails before the reconnect wait settles', async () => {
211
+ let rejectReconnect!: (error: Error) => void;
212
+ const waitForReconnect = vi.fn(
213
+ () =>
214
+ new Promise<void>((_, reject) => {
215
+ rejectReconnect = reject;
216
+ })
217
+ );
218
+ const connection = {
219
+ resetEnvironment: vi.fn(async () => {
220
+ throw new Error('rpc failed');
221
+ }),
222
+ };
223
+
224
+ const unhandled: unknown[] = [];
225
+ const onUnhandledRejection = (reason: unknown) => {
226
+ unhandled.push(reason);
227
+ };
228
+ process.on('unhandledRejection', onUnhandledRejection);
229
+
230
+ try {
231
+ const strategy = createRuntimeResetStrategy({
232
+ getConnection: () => connection,
233
+ expectDisconnect: () => () => undefined,
234
+ waitForReconnect,
235
+ isDisconnectError: () => false,
236
+ timeoutMs: 1000,
237
+ });
238
+
239
+ await expect(strategy.reset({ testFilePath })).rejects.toThrow('rpc failed');
240
+
241
+ // Simulate the reconnect timeout firing after the strategy already
242
+ // threw; the strategy must have attached a rejection handler.
243
+ rejectReconnect(new Error('reconnect timed out'));
244
+ await new Promise((resolve) => setImmediate(resolve));
245
+
246
+ expect(unhandled).toEqual([]);
247
+ } finally {
248
+ process.off('unhandledRejection', onUnhandledRejection);
249
+ }
250
+ });
251
+ });
252
+
253
+ describe('withEscalation', () => {
254
+ const testFilePath = '/tests/example.harness.ts';
255
+
256
+ const makeStrategy = (
257
+ kind: EnvironmentResetStrategy['kind'],
258
+ reset: EnvironmentResetStrategy['reset']
259
+ ): EnvironmentResetStrategy => ({ kind, reset });
260
+
261
+ it('reports the primary strategy kind', () => {
262
+ const primary = makeStrategy('runtime', async () => undefined);
263
+ const fallback = makeStrategy('process', async () => undefined);
264
+
265
+ expect(withEscalation(primary, fallback).kind).toBe('runtime');
266
+ });
267
+
268
+ it('does not call the fallback when the primary succeeds', async () => {
269
+ const fallbackReset = vi.fn(async () => undefined);
270
+ const primary = makeStrategy('runtime', async () => undefined);
271
+ const fallback = makeStrategy('process', fallbackReset);
272
+ const onEscalate = vi.fn();
273
+
274
+ await withEscalation(primary, fallback, { onEscalate }).reset({ testFilePath });
275
+
276
+ expect(fallbackReset).not.toHaveBeenCalled();
277
+ expect(onEscalate).not.toHaveBeenCalled();
278
+ });
279
+
280
+ it('escalates to the fallback when the primary fails', async () => {
281
+ const primaryError = new Error('runtime reset failed');
282
+ const fallbackReset = vi.fn(async () => undefined);
283
+ const primary = makeStrategy('runtime', async () => {
284
+ throw primaryError;
285
+ });
286
+ const fallback = makeStrategy('process', fallbackReset);
287
+ const onEscalate = vi.fn();
288
+
289
+ await withEscalation(primary, fallback, { onEscalate }).reset({ testFilePath });
290
+
291
+ expect(fallbackReset).toHaveBeenCalledWith({ testFilePath });
292
+ expect(onEscalate).toHaveBeenCalledWith(primaryError);
293
+ });
294
+
295
+ it('propagates the fallback error when both strategies fail', async () => {
296
+ const fallbackError = new Error('process reset also failed');
297
+ const primary = makeStrategy('runtime', async () => {
298
+ throw new Error('runtime reset failed');
299
+ });
300
+ const fallback = makeStrategy('process', async () => {
301
+ throw fallbackError;
302
+ });
303
+
304
+ await expect(
305
+ withEscalation(primary, fallback).reset({ testFilePath })
306
+ ).rejects.toBe(fallbackError);
307
+ });
308
+ });
@@ -1,4 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
+ import path from 'node:path';
3
+ import { AppBridgeDisconnectedError } from '@react-native-harness/bridge/server';
2
4
  import { NativeCrashError, PlatformReadyTimeoutError } from '../errors.js';
3
5
 
4
6
  describe('PlatformReadyTimeoutError', () => {
@@ -10,6 +12,62 @@ describe('PlatformReadyTimeoutError', () => {
10
12
  });
11
13
 
12
14
  describe('NativeCrashError', () => {
15
+ it('reports the extracted crash log path when available', () => {
16
+ const error = new NativeCrashError('/tmp/crash.harness.ts', {
17
+ phase: 'execution',
18
+ artifactPath: path.join(
19
+ process.cwd(),
20
+ '.harness',
21
+ 'crash-reports',
22
+ 'crash.ips'
23
+ ),
24
+ });
25
+
26
+ expect(error.message).toContain(
27
+ `Harness extracted the crash log: ${path.join(
28
+ '.harness',
29
+ 'crash-reports',
30
+ 'crash.ips'
31
+ )}`
32
+ );
33
+ });
34
+
35
+ it('lists enrichment artifact paths when available', () => {
36
+ const error = new NativeCrashError('/tmp/crash.harness.ts', {
37
+ phase: 'execution',
38
+ artifactPath: path.join(
39
+ process.cwd(),
40
+ '.harness',
41
+ 'crash-reports',
42
+ 'logcat.txt'
43
+ ),
44
+ enrichmentArtifacts: [
45
+ {
46
+ artifactType: 'dropbox-native-crash',
47
+ artifactPath: path.join(
48
+ process.cwd(),
49
+ '.harness',
50
+ 'crash-reports',
51
+ 'dropbox-native-crash.txt'
52
+ ),
53
+ },
54
+ ],
55
+ });
56
+
57
+ expect(error.message).toContain('Additional crash artifacts:');
58
+ expect(error.message).toContain(
59
+ path.join('.harness', 'crash-reports', 'dropbox-native-crash.txt')
60
+ );
61
+ });
62
+
63
+ it('reports crash log extraction failure when no artifact was pulled', () => {
64
+ const error = new NativeCrashError('/tmp/crash.harness.ts', {
65
+ phase: 'execution',
66
+ });
67
+
68
+ expect(error.message).toContain("Harness couldn't extract the crash log.");
69
+ });
70
+
13
71
  it('formats the extracted stack trace in the error message', () => {
14
72
  const error = new NativeCrashError('/tmp/crash.harness.ts', {
15
73
  phase: 'execution',
@@ -80,3 +138,20 @@ describe('NativeCrashError', () => {
80
138
  );
81
139
  });
82
140
  });
141
+
142
+ describe('AppBridgeDisconnectedError', () => {
143
+ it('explains likely causes without exposing an internal stack', () => {
144
+ const error = new AppBridgeDisconnectedError('app-disconnected');
145
+
146
+ expect(error.message).toBe(
147
+ 'The app bridge disconnected during test execution. This can happen if the app was killed, crashed, reloaded, or restarted while the test file was running.'
148
+ );
149
+ expect(error.stack).toBe(`AppBridgeDisconnectedError: ${error.message}`);
150
+ });
151
+
152
+ it('uses a reconnection-specific message when a newer app connection replaces the old one', () => {
153
+ expect(new AppBridgeDisconnectedError('app-replaced').message).toBe(
154
+ 'The app bridge was replaced by a newer app connection. This can happen when the app reloads, restarts, or reconnects while a test file is still running.'
155
+ );
156
+ });
157
+ });