@react-native-harness/jest 1.4.0-rc.1 → 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 (75) hide show
  1. package/dist/__tests__/bridge.test.js +6 -2
  2. package/dist/__tests__/crash-monitor.test.js +64 -0
  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__/execute-run.test.js +250 -4
  7. package/dist/__tests__/format-harness-error.test.d.ts +2 -0
  8. package/dist/__tests__/format-harness-error.test.d.ts.map +1 -0
  9. package/dist/__tests__/format-harness-error.test.js +27 -0
  10. package/dist/__tests__/harness-cache.test.js +62 -41
  11. package/dist/__tests__/harness-session-signal.test.d.ts +2 -0
  12. package/dist/__tests__/harness-session-signal.test.d.ts.map +1 -0
  13. package/dist/__tests__/harness-session-signal.test.js +298 -0
  14. package/dist/__tests__/harness-session.test.js +161 -2
  15. package/dist/__tests__/index.test.js +3 -1
  16. package/dist/__tests__/metro-port.test.js +2 -0
  17. package/dist/__tests__/run.test.d.ts +2 -0
  18. package/dist/__tests__/run.test.d.ts.map +1 -0
  19. package/dist/__tests__/run.test.js +48 -0
  20. package/dist/crash-monitor.d.ts +7 -0
  21. package/dist/crash-monitor.d.ts.map +1 -1
  22. package/dist/crash-monitor.js +19 -0
  23. package/dist/diagnostics/__tests__/derivers.test.d.ts +2 -0
  24. package/dist/diagnostics/__tests__/derivers.test.d.ts.map +1 -0
  25. package/dist/diagnostics/__tests__/derivers.test.js +248 -0
  26. package/dist/diagnostics/__tests__/report.test.d.ts +2 -0
  27. package/dist/diagnostics/__tests__/report.test.d.ts.map +1 -0
  28. package/dist/diagnostics/__tests__/report.test.js +97 -0
  29. package/dist/diagnostics/derivers.d.ts +24 -0
  30. package/dist/diagnostics/derivers.d.ts.map +1 -0
  31. package/dist/diagnostics/derivers.js +146 -0
  32. package/dist/diagnostics/index.d.ts +3 -0
  33. package/dist/diagnostics/index.d.ts.map +1 -0
  34. package/dist/diagnostics/index.js +2 -0
  35. package/dist/diagnostics/report.d.ts +7 -0
  36. package/dist/diagnostics/report.d.ts.map +1 -0
  37. package/dist/diagnostics/report.js +84 -0
  38. package/dist/environment-reset.d.ts +51 -0
  39. package/dist/environment-reset.d.ts.map +1 -0
  40. package/dist/environment-reset.js +83 -0
  41. package/dist/execute-run.d.ts.map +1 -1
  42. package/dist/execute-run.js +121 -69
  43. package/dist/format-harness-error.d.ts +5 -0
  44. package/dist/format-harness-error.d.ts.map +1 -0
  45. package/dist/format-harness-error.js +43 -0
  46. package/dist/harness-session.d.ts +26 -1
  47. package/dist/harness-session.d.ts.map +1 -1
  48. package/dist/harness-session.js +260 -87
  49. package/dist/run.d.ts.map +1 -1
  50. package/dist/run.js +21 -4
  51. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  52. package/package.json +8 -7
  53. package/src/__tests__/bridge.test.ts +8 -3
  54. package/src/__tests__/crash-monitor.test.ts +84 -0
  55. package/src/__tests__/environment-reset.test.ts +308 -0
  56. package/src/__tests__/execute-run.test.ts +327 -4
  57. package/src/__tests__/format-harness-error.test.ts +32 -0
  58. package/src/__tests__/harness-cache.test.ts +91 -0
  59. package/src/__tests__/harness-session.test.ts +211 -1
  60. package/src/__tests__/index.test.ts +3 -1
  61. package/src/__tests__/metro-port.test.ts +2 -0
  62. package/src/__tests__/run.test.ts +74 -0
  63. package/src/crash-monitor.ts +28 -0
  64. package/src/diagnostics/__tests__/derivers.test.ts +300 -0
  65. package/src/diagnostics/__tests__/report.test.ts +128 -0
  66. package/src/diagnostics/derivers.ts +170 -0
  67. package/src/diagnostics/index.ts +2 -0
  68. package/src/diagnostics/report.ts +124 -0
  69. package/src/environment-reset.ts +138 -0
  70. package/src/execute-run.ts +136 -77
  71. package/src/format-harness-error.ts +73 -0
  72. package/src/harness-session.ts +369 -131
  73. package/src/run.ts +23 -4
  74. package/tsconfig.json +3 -0
  75. package/tsconfig.lib.json +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-native-harness/jest",
3
- "version": "1.4.0-rc.1",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -39,12 +39,13 @@
39
39
  "jest-util": "^30.2.0",
40
40
  "tslib": "^2.3.0",
41
41
  "yargs": "^17.7.2",
42
- "@react-native-harness/plugins": "1.4.0-rc.1",
43
- "@react-native-harness/platforms": "1.4.0-rc.1",
44
- "@react-native-harness/bridge": "1.4.0-rc.1",
45
- "@react-native-harness/config": "1.4.0-rc.1",
46
- "@react-native-harness/tools": "1.4.0-rc.1",
47
- "@react-native-harness/bundler-metro": "1.4.0-rc.1"
42
+ "@react-native-harness/cache": "1.4.0",
43
+ "@react-native-harness/bundler-metro": "1.4.0",
44
+ "@react-native-harness/config": "1.4.0",
45
+ "@react-native-harness/bridge": "1.4.0",
46
+ "@react-native-harness/plugins": "1.4.0",
47
+ "@react-native-harness/platforms": "1.4.0",
48
+ "@react-native-harness/tools": "1.4.0"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@types/yargs": "^17.0.32",
@@ -37,8 +37,12 @@ afterEach(async () => {
37
37
  await new Promise((r) => setTimeout(r, 10));
38
38
  });
39
39
 
40
- const connect = (callbacks: Parameters<typeof connectToHarness>[1] = { runTests: vi.fn() }) =>
41
- connectToHarness(`ws://127.0.0.1:${bridgePort}`, callbacks);
40
+ const connect = (
41
+ callbacks: Parameters<typeof connectToHarness>[1] = {
42
+ runTests: vi.fn(),
43
+ resetEnvironment: vi.fn(),
44
+ },
45
+ ) => connectToHarness(`ws://127.0.0.1:${bridgePort}`, callbacks);
42
46
 
43
47
  const device = {
44
48
  platform: 'ios' as const,
@@ -140,7 +144,7 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
140
144
  const runTestsCb = vi.fn(async () => suiteResult);
141
145
 
142
146
  const connectionPromise = bridge.nextConnection();
143
- const handle = await connect({ runTests: runTestsCb });
147
+ const handle = await connect({ runTests: runTestsCb, resetEnvironment: vi.fn() });
144
148
  handle.reportReady(device);
145
149
 
146
150
  const conn = await connectionPromise;
@@ -157,6 +161,7 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
157
161
  const firstHandle = await connect({
158
162
  runTests: async (): Promise<TestSuiteResult> =>
159
163
  await new Promise<TestSuiteResult>(() => undefined),
164
+ resetEnvironment: vi.fn(),
160
165
  });
161
166
  firstHandle.reportReady(device);
162
167
 
@@ -236,4 +236,88 @@ describe('createCrashMonitor', () => {
236
236
  expect(session.removeListener).toHaveBeenCalledOnce();
237
237
  expect(cm.isAlive()).toBe(false);
238
238
  });
239
+
240
+ describe('expectDisconnect', () => {
241
+ it('suppresses handleBridgeDisconnect while the window is open', async () => {
242
+ const { session } = createAppSessionMock({ status: 'running' });
243
+ const cm = createCrashMonitor({ appSession: session });
244
+ const watch = cm.watch('test.ts', 'execution');
245
+ watch.promise.catch(noop);
246
+
247
+ const closeWindow = cm.expectDisconnect();
248
+ cm.handleBridgeDisconnect();
249
+ await waitForClassification();
250
+
251
+ const settled = vi.fn();
252
+ watch.promise.then(settled, settled);
253
+ await Promise.resolve();
254
+ expect(settled).not.toHaveBeenCalled();
255
+
256
+ closeWindow();
257
+ watch.cancel();
258
+ });
259
+
260
+ it('resumes classifying disconnects once the window closes', async () => {
261
+ const { session } = createAppSessionMock({ status: 'running' });
262
+ const cm = createCrashMonitor({ appSession: session });
263
+ const watch = cm.watch('test.ts', 'execution');
264
+ watch.promise.catch(noop);
265
+
266
+ const closeWindow = cm.expectDisconnect();
267
+ closeWindow();
268
+
269
+ cm.handleBridgeDisconnect();
270
+
271
+ await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
272
+ });
273
+
274
+ it('keeps the window open until every opener has closed it (nested windows)', async () => {
275
+ const { session } = createAppSessionMock({ status: 'running' });
276
+ const cm = createCrashMonitor({ appSession: session });
277
+ const watch = cm.watch('test.ts', 'execution');
278
+ watch.promise.catch(noop);
279
+
280
+ const closeFirst = cm.expectDisconnect();
281
+ const closeSecond = cm.expectDisconnect();
282
+ closeFirst();
283
+
284
+ cm.handleBridgeDisconnect();
285
+ await waitForClassification();
286
+
287
+ const settled = vi.fn();
288
+ watch.promise.then(settled, settled);
289
+ await Promise.resolve();
290
+ expect(settled).not.toHaveBeenCalled();
291
+
292
+ closeSecond();
293
+ watch.cancel();
294
+ });
295
+
296
+ it('ignores a redundant close() call', async () => {
297
+ const { session } = createAppSessionMock({ status: 'running' });
298
+ const cm = createCrashMonitor({ appSession: session });
299
+ const watch = cm.watch('test.ts', 'execution');
300
+ watch.promise.catch(noop);
301
+
302
+ const closeWindow = cm.expectDisconnect();
303
+ closeWindow();
304
+ closeWindow();
305
+
306
+ cm.handleBridgeDisconnect();
307
+
308
+ await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
309
+ });
310
+
311
+ it('clears the expected-disconnect window on dispose', async () => {
312
+ const { session } = createAppSessionMock({ status: 'running' });
313
+ const cm = createCrashMonitor({ appSession: session });
314
+ cm.expectDisconnect();
315
+
316
+ await cm.dispose();
317
+
318
+ // A disconnect after dispose should not throw or hang; monitoring is
319
+ // fully torn down regardless of the window state.
320
+ expect(() => cm.handleBridgeDisconnect()).not.toThrow();
321
+ });
322
+ });
239
323
  });
@@ -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
+ });