@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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Integration test pairing createHarnessBridge (CLI side) with
3
3
  * connectToHarness (app side). Tests the full connection lifecycle and
4
- * RPC round-trip without knowing birpc internals.
4
+ * RPC round-trip without coupling tests to the transport internals.
5
5
  */
6
6
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
7
  import { createHarnessBridge } from '@react-native-harness/bridge/server';
@@ -28,7 +28,10 @@ afterEach(async () => {
28
28
  // Allow the server to close cleanly.
29
29
  await new Promise((r) => setTimeout(r, 10));
30
30
  });
31
- const connect = (callbacks = { runTests: vi.fn() }) => connectToHarness(`ws://127.0.0.1:${bridgePort}`, callbacks);
31
+ const connect = (callbacks = {
32
+ runTests: vi.fn(),
33
+ resetEnvironment: vi.fn(),
34
+ }) => connectToHarness(`ws://127.0.0.1:${bridgePort}`, callbacks);
32
35
  const device = {
33
36
  platform: 'ios',
34
37
  manufacturer: 'Apple',
@@ -107,7 +110,7 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
107
110
  };
108
111
  const runTestsCb = vi.fn(async () => suiteResult);
109
112
  const connectionPromise = bridge.nextConnection();
110
- const handle = await connect({ runTests: runTestsCb });
113
+ const handle = await connect({ runTests: runTestsCb, resetEnvironment: vi.fn() });
111
114
  handle.reportReady(device);
112
115
  const conn = await connectionPromise;
113
116
  const result = await conn.runTests('example.ts', {
@@ -117,6 +120,28 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
117
120
  expect(result.tests[0].name).toBe('passes');
118
121
  handle.disconnect();
119
122
  });
123
+ it('rejects pending app calls when a newer client replaces the session', async () => {
124
+ const firstHandle = await connect({
125
+ runTests: async () => await new Promise(() => undefined),
126
+ resetEnvironment: vi.fn(),
127
+ });
128
+ firstHandle.reportReady(device);
129
+ const firstConnection = await bridge.nextConnection();
130
+ const pendingRun = firstConnection.runTests('example.ts', {
131
+ runner: '/runner.js',
132
+ });
133
+ const pendingRunAssertion = expect(pendingRun).rejects.toThrow('The app bridge was replaced by a newer app connection.');
134
+ const secondHandle = await connect();
135
+ secondHandle.reportReady({
136
+ ...device,
137
+ model: 'iPhone 17 Pro Replacement',
138
+ });
139
+ await pendingRunAssertion;
140
+ await new Promise((r) => setTimeout(r, 20));
141
+ expect(bridge.connection?.device.model).toBe('iPhone 17 Pro Replacement');
142
+ secondHandle.disconnect();
143
+ firstHandle.disconnect();
144
+ });
120
145
  });
121
146
  describe('bridge events', () => {
122
147
  it('emitEvent on app side fires the event listener on bridge', async () => {
@@ -136,7 +161,7 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
136
161
  it('rejects pending nextConnection() waiters', async () => {
137
162
  const pending = bridge.nextConnection();
138
163
  bridge.dispose();
139
- await expect(pending).rejects.toThrow('Bridge disposed');
164
+ await expect(pending).rejects.toThrow('The app bridge was disposed before the test file finished running.');
140
165
  });
141
166
  });
142
167
  });
@@ -1,212 +1,230 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
+ import path from 'node:path';
2
3
  import { createCrashMonitor, CrashWatchCancelledError, } from '../crash-monitor.js';
3
- import { NativeCrashError } from '../errors.js';
4
+ import { NativeCrashError, RuntimeDisconnectError } from '../errors.js';
4
5
  const noop = () => undefined;
5
- const resolveUndefined = async () => undefined;
6
- // ---------------------------------------------------------------------------
7
- // Test doubles
8
- // ---------------------------------------------------------------------------
9
- const createAppMonitorMock = () => {
10
- let registeredListener = null;
11
- const monitor = {
12
- start: vi.fn(resolveUndefined),
13
- stop: vi.fn(resolveUndefined),
14
- dispose: vi.fn(resolveUndefined),
6
+ const waitForClassification = () => new Promise((resolve) => setTimeout(resolve, 1600));
7
+ const createAppSessionMock = (initialState = { status: 'running' }, logs = [], getCrashDetails) => {
8
+ let state = initialState;
9
+ let listener = null;
10
+ const session = {
11
+ dispose: vi.fn(async () => undefined),
12
+ getState: vi.fn(async () => state),
13
+ getLogs: vi.fn(() => logs),
15
14
  addListener: vi.fn((l) => {
16
- registeredListener = l;
15
+ listener = l;
17
16
  }),
18
17
  removeListener: vi.fn((l) => {
19
- if (registeredListener === l)
20
- registeredListener = null;
18
+ if (listener === l)
19
+ listener = null;
21
20
  }),
22
21
  };
22
+ if (getCrashDetails) {
23
+ session.getCrashDetails = getCrashDetails;
24
+ }
23
25
  return {
24
- monitor,
25
- emit: (event) => registeredListener?.(event),
26
+ session,
27
+ setState: (nextState) => {
28
+ state = nextState;
29
+ },
30
+ emit: (event) => listener?.(event),
26
31
  };
27
32
  };
28
- const createPlatformRunnerMock = (isRunning = false, crashDetails = null) => ({
29
- isAppRunning: vi.fn(async () => isRunning),
30
- getCrashDetails: vi.fn(async () => crashDetails),
31
- startApp: vi.fn(resolveUndefined),
32
- restartApp: vi.fn(resolveUndefined),
33
- stopApp: vi.fn(resolveUndefined),
34
- dispose: vi.fn(resolveUndefined),
35
- createAppMonitor: vi.fn(),
36
- });
37
- // ---------------------------------------------------------------------------
38
- // Tests
39
- // ---------------------------------------------------------------------------
40
33
  describe('createCrashMonitor', () => {
41
- describe('liveness', () => {
42
- it('starts not alive', () => {
43
- const { monitor } = createAppMonitorMock();
44
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
45
- expect(cm.isAlive()).toBe(false);
46
- });
47
- it('becomes alive when the app starts', () => {
48
- const { monitor, emit } = createAppMonitorMock();
49
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
50
- emit({ type: 'app_started' });
51
- expect(cm.isAlive()).toBe(true);
52
- });
53
- it('becomes not alive after a confirmed crash', async () => {
54
- const { monitor, emit } = createAppMonitorMock();
55
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
56
- emit({ type: 'app_started' });
57
- const watch = cm.watch('test.ts', 'execution');
58
- watch.promise.catch(noop);
59
- emit({ type: 'app_exited', isConfirmed: true });
60
- await watch.promise.catch(noop);
61
- expect(cm.isAlive()).toBe(false);
62
- });
34
+ it('starts not alive and becomes alive when an app session is attached', () => {
35
+ const cm = createCrashMonitor();
36
+ const { session } = createAppSessionMock();
37
+ expect(cm.isAlive()).toBe(false);
38
+ cm.setAppSession(session);
39
+ expect(cm.isAlive()).toBe(true);
63
40
  });
64
- describe('watch', () => {
65
- it('promise rejects with NativeCrashError on confirmed app_exited', async () => {
66
- const { monitor, emit } = createAppMonitorMock();
67
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
68
- const watch = cm.watch('/test/example.ts', 'execution');
69
- watch.promise.catch(noop);
70
- emit({ type: 'app_exited', isConfirmed: true });
71
- await expect(watch.promise).rejects.toBeInstanceOf(NativeCrashError);
72
- });
73
- it('attributes the crash to the file and phase passed to watch()', async () => {
74
- const { monitor, emit } = createAppMonitorMock();
75
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
76
- const watch = cm.watch('/test/example.ts', 'startup');
77
- watch.promise.catch(noop);
78
- emit({ type: 'app_exited', isConfirmed: true });
79
- const error = await watch.promise.catch((e) => e);
80
- expect(error.testFilePath).toBe('/test/example.ts');
81
- expect(error.details.phase).toBe('startup');
82
- });
83
- it('settles the promise with CrashWatchCancelledError on cancel()', async () => {
84
- const { monitor } = createAppMonitorMock();
85
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
86
- const watch = cm.watch('test.ts', 'execution');
87
- watch.cancel();
88
- await expect(watch.promise).rejects.toBeInstanceOf(CrashWatchCancelledError);
89
- });
90
- it('subsequent cancel() after crash is a no-op', async () => {
91
- const { monitor, emit } = createAppMonitorMock();
92
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
93
- const watch = cm.watch('test.ts', 'execution');
94
- watch.promise.catch(noop);
95
- emit({ type: 'app_exited', isConfirmed: true });
96
- await watch.promise.catch(noop);
97
- // Second cancel should not throw or cause issues.
98
- expect(() => watch.cancel()).not.toThrow();
99
- });
41
+ it('rejects a watch with NativeCrashError when the app session exits', async () => {
42
+ const { session, emit } = createAppSessionMock({
43
+ status: 'exited',
44
+ occurredAt: Date.now(),
45
+ reason: 'observed-exit',
46
+ });
47
+ const cm = createCrashMonitor({ appSession: session });
48
+ const watch = cm.watch('/test/example.ts', 'execution');
49
+ watch.promise.catch(noop);
50
+ emit({ type: 'app_exited' });
51
+ const error = await watch.promise.catch((err) => err);
52
+ expect(error).toBeInstanceOf(NativeCrashError);
53
+ expect(error.testFilePath).toBe('/test/example.ts');
54
+ expect(error.details.phase).toBe('execution');
55
+ expect(cm.isAlive()).toBe(false);
56
+ });
57
+ it('classifies bridge disconnect plus exited app as NativeCrashError', async () => {
58
+ const { session, setState } = createAppSessionMock();
59
+ const cm = createCrashMonitor({ appSession: session });
60
+ const watch = cm.watch('test.ts', 'execution');
61
+ watch.promise.catch(noop);
62
+ cm.handleBridgeDisconnect();
63
+ setState({
64
+ status: 'exited',
65
+ occurredAt: Date.now(),
66
+ reason: 'process-gone',
67
+ });
68
+ await expect(watch.promise).rejects.toBeInstanceOf(NativeCrashError);
69
+ });
70
+ it('classifies bridge disconnect plus running app as RuntimeDisconnectError', async () => {
71
+ const { session } = createAppSessionMock({ status: 'running' });
72
+ const cm = createCrashMonitor({ appSession: session });
73
+ const watch = cm.watch('test.ts', 'execution');
74
+ watch.promise.catch(noop);
75
+ cm.handleBridgeDisconnect();
76
+ await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
77
+ });
78
+ it('attaches matching session log evidence to native crash details', async () => {
79
+ const occurredAt = Date.now();
80
+ const logs = [
81
+ { line: 'ordinary app log', occurredAt },
82
+ { line: 'MyApp[123] fatal error: boom', occurredAt },
83
+ ];
84
+ const { session, setState } = createAppSessionMock({ status: 'running' }, logs);
85
+ const cm = createCrashMonitor({ appSession: session });
86
+ const watch = cm.watch('test.ts', 'execution');
87
+ watch.promise.catch(noop);
88
+ cm.handleBridgeDisconnect();
89
+ setState({ status: 'exited', occurredAt, reason: 'process-gone' });
90
+ const error = await watch.promise.catch((err) => err);
91
+ expect(error).toBeInstanceOf(NativeCrashError);
92
+ expect(error.details.rawLines).toEqual(['MyApp[123] fatal error: boom']);
93
+ });
94
+ it('asks the app session to extract native crash details for the current test', async () => {
95
+ const occurredAt = Date.now();
96
+ const getCrashDetails = vi.fn(async () => ({
97
+ artifactType: 'logcat',
98
+ artifactPath: '/tmp/.harness/crash-reports/crash-logcat.txt',
99
+ processName: 'com.harnessplayground',
100
+ pid: 7777,
101
+ }));
102
+ const { session, setState } = createAppSessionMock({ status: 'running' }, [], getCrashDetails);
103
+ const cm = createCrashMonitor({ appSession: session });
104
+ const watch = cm.watch('/test/example.ts', 'execution');
105
+ watch.promise.catch(noop);
106
+ cm.handleBridgeDisconnect();
107
+ setState({
108
+ status: 'exited',
109
+ occurredAt,
110
+ pid: 7777,
111
+ reason: 'process-gone',
112
+ });
113
+ const error = await watch.promise.catch((err) => err);
114
+ expect(getCrashDetails).toHaveBeenCalledWith({
115
+ occurredAt: expect.any(Number),
116
+ pid: 7777,
117
+ processName: undefined,
118
+ testFilePath: '/test/example.ts',
119
+ });
120
+ expect(error.details.artifactPath).toBe('/tmp/.harness/crash-reports/crash-logcat.txt');
121
+ expect(error.message).toContain(`Harness extracted the crash log: ${path.relative(process.cwd(), '/tmp/.harness/crash-reports/crash-logcat.txt')}`);
122
+ });
123
+ it('falls back to log details when native crash extraction fails', async () => {
124
+ const occurredAt = Date.now();
125
+ const getCrashDetails = vi.fn(async () => {
126
+ throw new Error('copy failed');
127
+ });
128
+ const { session, setState } = createAppSessionMock({ status: 'running' }, [{ line: 'MyApp[123] fatal error: boom', occurredAt }], getCrashDetails);
129
+ const cm = createCrashMonitor({ appSession: session });
130
+ const watch = cm.watch('/test/example.ts', 'execution');
131
+ watch.promise.catch(noop);
132
+ cm.handleBridgeDisconnect();
133
+ setState({ status: 'exited', occurredAt, reason: 'process-gone' });
134
+ const error = await watch.promise.catch((err) => err);
135
+ expect(error.details.rawLines).toEqual(['MyApp[123] fatal error: boom']);
136
+ expect(error.message).toContain("Harness couldn't extract the crash log.");
137
+ });
138
+ it('settles the promise with CrashWatchCancelledError on cancel()', async () => {
139
+ const cm = createCrashMonitor();
140
+ const watch = cm.watch('test.ts', 'execution');
141
+ watch.cancel();
142
+ await expect(watch.promise).rejects.toBeInstanceOf(CrashWatchCancelledError);
100
143
  });
101
- describe('unconfirmed events', () => {
102
- it('fires the crash if isAppRunning returns false', async () => {
103
- const { monitor, emit } = createAppMonitorMock();
104
- const runner = createPlatformRunnerMock(false /* not running */);
105
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: runner });
144
+ it('ignores session events while stopped', async () => {
145
+ const { session, emit } = createAppSessionMock({
146
+ status: 'exited',
147
+ occurredAt: Date.now(),
148
+ });
149
+ const cm = createCrashMonitor({ appSession: session });
150
+ await cm.stop();
151
+ const watch = cm.watch('test.ts', 'execution');
152
+ const settled = vi.fn();
153
+ watch.promise.then(settled, settled);
154
+ emit({ type: 'app_exited' });
155
+ await waitForClassification();
156
+ expect(settled).not.toHaveBeenCalled();
157
+ watch.cancel();
158
+ });
159
+ it('removes the app session listener on dispose', async () => {
160
+ const { session } = createAppSessionMock();
161
+ const cm = createCrashMonitor({ appSession: session });
162
+ await cm.dispose();
163
+ expect(session.removeListener).toHaveBeenCalledOnce();
164
+ expect(cm.isAlive()).toBe(false);
165
+ });
166
+ describe('expectDisconnect', () => {
167
+ it('suppresses handleBridgeDisconnect while the window is open', async () => {
168
+ const { session } = createAppSessionMock({ status: 'running' });
169
+ const cm = createCrashMonitor({ appSession: session });
106
170
  const watch = cm.watch('test.ts', 'execution');
107
171
  watch.promise.catch(noop);
108
- emit({ type: 'app_exited', isConfirmed: false });
109
- await expect(watch.promise).rejects.toBeInstanceOf(NativeCrashError);
110
- });
111
- it('does not fire if isAppRunning returns true', async () => {
112
- const { monitor, emit } = createAppMonitorMock();
113
- const runner = createPlatformRunnerMock(true /* still running */);
114
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: runner });
115
- const watch = cm.watch('test.ts', 'execution');
172
+ const closeWindow = cm.expectDisconnect();
173
+ cm.handleBridgeDisconnect();
174
+ await waitForClassification();
116
175
  const settled = vi.fn();
117
176
  watch.promise.then(settled, settled);
118
- emit({ type: 'app_exited', isConfirmed: false });
119
- await new Promise((r) => setTimeout(r, 20));
177
+ await Promise.resolve();
120
178
  expect(settled).not.toHaveBeenCalled();
179
+ closeWindow();
121
180
  watch.cancel();
122
181
  });
123
- it('fires on possible_crash when confirmed', async () => {
124
- const { monitor, emit } = createAppMonitorMock();
125
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
182
+ it('resumes classifying disconnects once the window closes', async () => {
183
+ const { session } = createAppSessionMock({ status: 'running' });
184
+ const cm = createCrashMonitor({ appSession: session });
126
185
  const watch = cm.watch('test.ts', 'execution');
127
186
  watch.promise.catch(noop);
128
- emit({ type: 'possible_crash', isConfirmed: true });
129
- await expect(watch.promise).rejects.toBeInstanceOf(NativeCrashError);
130
- });
131
- });
132
- describe('crash detail enrichment', () => {
133
- it('merges initial and enriched crash details', async () => {
134
- const { monitor, emit } = createAppMonitorMock();
135
- const runner = createPlatformRunnerMock(false, {
136
- processName: 'MyApp',
137
- signal: 'SIGSEGV',
138
- summary: 'Segmentation fault',
139
- });
140
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: runner });
187
+ const closeWindow = cm.expectDisconnect();
188
+ closeWindow();
189
+ cm.handleBridgeDisconnect();
190
+ await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
191
+ });
192
+ it('keeps the window open until every opener has closed it (nested windows)', async () => {
193
+ const { session } = createAppSessionMock({ status: 'running' });
194
+ const cm = createCrashMonitor({ appSession: session });
141
195
  const watch = cm.watch('test.ts', 'execution');
142
196
  watch.promise.catch(noop);
143
- emit({ type: 'app_exited', isConfirmed: true, crashDetails: { pid: 1234 } });
144
- const error = await watch.promise.catch((e) => e);
145
- expect(error.details.processName).toBe('MyApp');
146
- expect(error.details.signal).toBe('SIGSEGV');
147
- expect(error.details.pid).toBe(1234);
148
- });
149
- });
150
- describe('stop / start', () => {
151
- it('ignores events while stopped', async () => {
152
- const { monitor, emit } = createAppMonitorMock();
153
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
154
- await cm.stop();
155
- const watch = cm.watch('test.ts', 'execution');
197
+ const closeFirst = cm.expectDisconnect();
198
+ const closeSecond = cm.expectDisconnect();
199
+ closeFirst();
200
+ cm.handleBridgeDisconnect();
201
+ await waitForClassification();
156
202
  const settled = vi.fn();
157
203
  watch.promise.then(settled, settled);
158
- emit({ type: 'app_exited', isConfirmed: true });
159
- await new Promise((r) => setTimeout(r, 10));
204
+ await Promise.resolve();
160
205
  expect(settled).not.toHaveBeenCalled();
206
+ closeSecond();
161
207
  watch.cancel();
162
208
  });
163
- it('resumes monitoring after start()', async () => {
164
- const { monitor, emit } = createAppMonitorMock();
165
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
166
- await cm.stop();
167
- await cm.start();
209
+ it('ignores a redundant close() call', async () => {
210
+ const { session } = createAppSessionMock({ status: 'running' });
211
+ const cm = createCrashMonitor({ appSession: session });
168
212
  const watch = cm.watch('test.ts', 'execution');
169
213
  watch.promise.catch(noop);
170
- emit({ type: 'app_exited', isConfirmed: true });
171
- await expect(watch.promise).rejects.toBeInstanceOf(NativeCrashError);
172
- });
173
- });
174
- describe('reset', () => {
175
- it('clears alive state and pending watchers', async () => {
176
- const { monitor, emit } = createAppMonitorMock();
177
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
178
- emit({ type: 'app_started' });
179
- const watch = cm.watch('test.ts', 'execution');
180
- watch.promise.catch(noop);
181
- cm.reset();
182
- expect(cm.isAlive()).toBe(false);
183
- // The watcher was cleared; a crash fired now should not reach the old watch.
184
- emit({ type: 'app_exited', isConfirmed: true });
185
- await new Promise((r) => setTimeout(r, 10));
186
- // Old promise is still pending (we can verify by cancel resolving it).
187
- watch.cancel();
188
- await expect(watch.promise).rejects.toBeInstanceOf(CrashWatchCancelledError);
189
- });
190
- });
191
- describe('dispose', () => {
192
- it('ignores events after dispose', async () => {
193
- const { monitor, emit } = createAppMonitorMock();
194
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
195
- const watch = cm.watch('test.ts', 'execution');
196
- watch.promise.catch(noop);
197
- await cm.dispose();
198
- emit({ type: 'app_exited', isConfirmed: true });
199
- await new Promise((r) => setTimeout(r, 10));
200
- // After dispose watchers are cleared, so crash didn't propagate.
201
- // The promise is still pending - cancel to settle it.
202
- watch.cancel();
203
- await expect(watch.promise).rejects.toBeInstanceOf(CrashWatchCancelledError);
204
- });
205
- it('calls appMonitor.dispose()', async () => {
206
- const { monitor } = createAppMonitorMock();
207
- const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
214
+ const closeWindow = cm.expectDisconnect();
215
+ closeWindow();
216
+ closeWindow();
217
+ cm.handleBridgeDisconnect();
218
+ await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
219
+ });
220
+ it('clears the expected-disconnect window on dispose', async () => {
221
+ const { session } = createAppSessionMock({ status: 'running' });
222
+ const cm = createCrashMonitor({ appSession: session });
223
+ cm.expectDisconnect();
208
224
  await cm.dispose();
209
- expect(monitor.dispose).toHaveBeenCalledOnce();
225
+ // A disconnect after dispose should not throw or hang; monitoring is
226
+ // fully torn down regardless of the window state.
227
+ expect(() => cm.handleBridgeDisconnect()).not.toThrow();
210
228
  });
211
229
  });
212
230
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=environment-reset.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment-reset.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/environment-reset.test.ts"],"names":[],"mappings":""}