@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
@@ -11,6 +11,7 @@ import {
11
11
  AppBridgeDisconnectedError,
12
12
  DeviceNotRespondingError,
13
13
  } from '@react-native-harness/bridge/server';
14
+ import { createDiagnostics } from '@react-native-harness/tools';
14
15
  import type { HarnessSession } from '../harness-session.js';
15
16
  import { executeRun } from '../execute-run.js';
16
17
 
@@ -29,6 +30,16 @@ vi.mock('../run.js', () => ({
29
30
  runHarnessTestFile: mockRunHarnessTestFile,
30
31
  }));
31
32
 
33
+ // Mock the reporting module so diagnostics tests can inspect the entries that
34
+ // would have been reported without touching the real filesystem, and so
35
+ // asserting on them doesn't race with executeRun's own diagnostics.flush().
36
+ const mockPrintSummary = vi.hoisted(() => vi.fn());
37
+ const mockWriteTraceFile = vi.hoisted(() => vi.fn(async () => '/fake/trace.json'));
38
+ vi.mock('../diagnostics/index.js', () => ({
39
+ printSummary: mockPrintSummary,
40
+ writeTraceFile: mockWriteTraceFile,
41
+ }));
42
+
32
43
  // ---------------------------------------------------------------------------
33
44
  // Helpers
34
45
  // ---------------------------------------------------------------------------
@@ -112,9 +123,11 @@ const makeSession = (overrides: Partial<HarnessSession> = {}): HarnessSession =>
112
123
  config: {},
113
124
  },
114
125
  } as HarnessSession['context'],
126
+ diagnostics: createDiagnostics({ enabled: false }),
115
127
  ensureAppReady: vi.fn(resolveUndefined),
116
128
  runTestFile: vi.fn(async () => makeHarnessResult()),
117
129
  restartApp: vi.fn(resolveUndefined),
130
+ resetEnvironment: vi.fn(resolveUndefined),
118
131
  resetCrashState: vi.fn(),
119
132
  flushClientLogs: vi.fn(() => []),
120
133
  callHook: vi.fn(resolveUndefined),
@@ -186,13 +199,68 @@ describe('executeRun', () => {
186
199
  });
187
200
  });
188
201
 
202
+ describe('diagnostics', () => {
203
+ beforeEach(() => {
204
+ mockPrintSummary.mockClear();
205
+ mockWriteTraceFile.mockClear();
206
+ });
207
+
208
+ it('records run.total and run.file spans when diagnostics is enabled', async () => {
209
+ const diagnostics = createDiagnostics({ enabled: true });
210
+ const session = makeSession({ diagnostics });
211
+
212
+ await executeRun(session, [makeTest('/a.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
213
+
214
+ // executeRun flushes and hands entries to the (mocked) reporter in its
215
+ // finally block, so we inspect what was reported rather than flushing
216
+ // the diagnostics instance ourselves.
217
+ expect(mockPrintSummary).toHaveBeenCalledTimes(1);
218
+ const entries = mockPrintSummary.mock.calls[0]?.[0] as Array<{ label: string; status: string; attrs?: Record<string, unknown> }>;
219
+ const runEntry = entries.find((e) => e.label === 'run.total');
220
+ const fileEntry = entries.find((e) => e.label === 'run.file');
221
+
222
+ expect(runEntry).toMatchObject({ status: 'ok', attrs: { status: 'passed' } });
223
+ expect(runEntry?.attrs?.runId).toBeTypeOf('string');
224
+ expect(fileEntry).toMatchObject({ status: 'ok', attrs: { file: '../a.ts', status: 'passed' } });
225
+ expect(fileEntry?.attrs?.runId).toBeTypeOf('string');
226
+ expect(mockWriteTraceFile).toHaveBeenCalledWith(entries, expect.objectContaining({ runId: expect.any(String) }));
227
+ });
228
+
229
+ it('still records run.total when a test throws', async () => {
230
+ const diagnostics = createDiagnostics({ enabled: true });
231
+ const session = makeSession({
232
+ diagnostics,
233
+ ensureAppReady: vi.fn(async () => { throw new Error('unexpected'); }),
234
+ });
235
+
236
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
237
+
238
+ const entries = mockPrintSummary.mock.calls[0]?.[0] as Array<{ label: string; attrs?: Record<string, unknown> }>;
239
+ expect(entries.find((e) => e.label === 'run.total')).toBeDefined();
240
+ expect(entries.find((e) => e.label === 'run.file')).toMatchObject({
241
+ attrs: { status: 'failed' },
242
+ });
243
+ });
244
+
245
+ it('is a no-op and does not generate a report when diagnostics is disabled', async () => {
246
+ const diagnostics = createDiagnostics({ enabled: false });
247
+ const session = makeSession({ diagnostics });
248
+
249
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
250
+
251
+ expect(diagnostics.flush()).toEqual([]);
252
+ expect(mockPrintSummary).not.toHaveBeenCalled();
253
+ expect(mockWriteTraceFile).not.toHaveBeenCalled();
254
+ });
255
+ });
256
+
189
257
  describe('happy path', () => {
190
258
  it('emits file start, ensureAppReady, runTestFile, file success in order', async () => {
191
259
  const order: string[] = [];
192
260
  const session = makeSession({
193
261
  ensureAppReady: vi.fn(async () => { order.push('ensureAppReady'); }),
194
262
  });
195
- const emitEvent: EmitEvent = (async (eventName, ..._eventData) => {
263
+ const emitEvent: EmitEvent = (async (eventName) => {
196
264
  if (eventName === 'test-file-start') {
197
265
  order.push('test-file-start');
198
266
  }
@@ -236,6 +304,25 @@ describe('executeRun', () => {
236
304
  expect(payload.status).toBe('passed');
237
305
  });
238
306
 
307
+ it('marks the final run state as completed', async () => {
308
+ const session = makeSession();
309
+
310
+ await executeRun(
311
+ session,
312
+ [makeTest()],
313
+ makeWatcher(),
314
+ makeEmitEvent().emitEvent,
315
+ makeGlobalConfig(),
316
+ );
317
+
318
+ expect(session.setRunState).toHaveBeenLastCalledWith(
319
+ expect.objectContaining({
320
+ completed: true,
321
+ status: 'passed',
322
+ }),
323
+ );
324
+ });
325
+
239
326
  it('attaches buffered client logs to the Jest result', async () => {
240
327
  const clientLogs = [{ message: 'Loaded screen', origin: '', type: 'warn' }] satisfies NonNullable<JestTestResult['console']>;
241
328
  const session = makeSession({
@@ -330,6 +417,65 @@ describe('executeRun', () => {
330
417
  ['test-file-success', expect.anything(), expect.anything()],
331
418
  ]);
332
419
  });
420
+
421
+ it('includes pending promise diagnostics in live test-case failures', async () => {
422
+ let testRunnerListener:
423
+ | ((event: TestRunnerTestStartedEvent | TestRunnerTestFinishedEvent) => void)
424
+ | undefined;
425
+ const { emitEvent, calls: emittedEvents } = makeEmitEvent();
426
+ const session = makeSession({
427
+ onTestRunnerEvent: vi.fn((listener) => {
428
+ testRunnerListener = listener as typeof testRunnerListener;
429
+ return () => undefined;
430
+ }),
431
+ });
432
+
433
+ mockRunHarnessTestFile.mockImplementation(async () => {
434
+ testRunnerListener?.({
435
+ type: 'test-finished',
436
+ file: 'example.ts',
437
+ suite: 'suite',
438
+ name: 'hangs',
439
+ ancestorTitles: ['suite'],
440
+ fullName: 'suite hangs',
441
+ startedAt: 100,
442
+ duration: 50,
443
+ status: 'failed',
444
+ error: {
445
+ name: 'TestCaseTimeoutError',
446
+ message: 'Test timed out after 50ms: suite hangs',
447
+ diagnostics: {
448
+ pendingPromises: {
449
+ total: 1,
450
+ items: [
451
+ {
452
+ id: 7,
453
+ createdAt: 110,
454
+ stack: 'Error: Promise created\n at hangs (example.ts:10:5)',
455
+ },
456
+ ],
457
+ },
458
+ },
459
+ },
460
+ });
461
+
462
+ return makeFileRunResult();
463
+ });
464
+
465
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
466
+
467
+ expect(emittedEvents).toContainEqual([
468
+ 'test-case-result',
469
+ 'example.ts',
470
+ expect.objectContaining({
471
+ failureMessages: [
472
+ expect.stringContaining(
473
+ 'Pending promises at timeout: 1\n\nPromise #7, created 10ms after test start:',
474
+ ),
475
+ ],
476
+ }),
477
+ ]);
478
+ });
333
479
  });
334
480
 
335
481
  describe('runtime failures', () => {
@@ -437,7 +583,7 @@ describe('executeRun', () => {
437
583
  });
438
584
 
439
585
  describe('resetEnvironmentBetweenTestFiles', () => {
440
- it('calls restartApp before the second test but not the first', async () => {
586
+ it('calls resetEnvironment before the second test but not the first', async () => {
441
587
  const session = makeSession({
442
588
  config: {
443
589
  metroPort: 8081,
@@ -458,8 +604,184 @@ describe('executeRun', () => {
458
604
  makeGlobalConfig(),
459
605
  );
460
606
 
461
- // restartApp should be called for tests 2 and 3, not test 1.
462
- expect(session.restartApp).toHaveBeenCalledTimes(2);
607
+ // resetEnvironment should be called for tests 2 and 3, not test 1.
608
+ expect(session.resetEnvironment).toHaveBeenCalledTimes(2);
609
+ expect(session.restartApp).not.toHaveBeenCalled();
610
+ });
611
+
612
+ it('calls resetEnvironment with the "runtime" strategy configured', async () => {
613
+ const session = makeSession({
614
+ config: {
615
+ metroPort: 8081,
616
+ resetEnvironmentBetweenTestFiles: 'runtime',
617
+ detectNativeCrashes: false,
618
+ runners: [
619
+ { platformId: 'android', name: 'android' },
620
+ { platformId: 'ios', name: 'ios' },
621
+ ],
622
+ } as HarnessSession['config'],
623
+ });
624
+
625
+ await executeRun(
626
+ session,
627
+ [makeTest('/a.ts'), makeTest('/b.ts')],
628
+ makeWatcher(),
629
+ makeEmitEvent().emitEvent,
630
+ makeGlobalConfig(),
631
+ );
632
+
633
+ expect(session.resetEnvironment).toHaveBeenCalledTimes(1);
634
+ expect(session.resetEnvironment).toHaveBeenCalledWith('/b.ts');
635
+ });
636
+
637
+ it('does not call resetEnvironment when resetEnvironmentBetweenTestFiles is false', async () => {
638
+ const session = makeSession({
639
+ config: {
640
+ metroPort: 8081,
641
+ resetEnvironmentBetweenTestFiles: false,
642
+ detectNativeCrashes: false,
643
+ runners: [
644
+ { platformId: 'android', name: 'android' },
645
+ { platformId: 'ios', name: 'ios' },
646
+ ],
647
+ } as HarnessSession['config'],
648
+ });
649
+
650
+ await executeRun(
651
+ session,
652
+ [makeTest('/a.ts'), makeTest('/b.ts')],
653
+ makeWatcher(),
654
+ makeEmitEvent().emitEvent,
655
+ makeGlobalConfig(),
656
+ );
657
+
658
+ expect(session.resetEnvironment).not.toHaveBeenCalled();
659
+ });
660
+
661
+ it('restarts after a test case timeout before the next runnable file', async () => {
662
+ const timedOutResult = makeHarnessResult('failed');
663
+ timedOutResult.tests = [
664
+ {
665
+ name: 'hangs',
666
+ status: 'failed',
667
+ duration: 10,
668
+ error: {
669
+ name: 'TestCaseTimeoutError',
670
+ message: 'Test timed out after 10ms: hangs',
671
+ },
672
+ },
673
+ ];
674
+ mockRunHarnessTestFile
675
+ .mockResolvedValueOnce(makeFileRunResult({
676
+ harnessResult: timedOutResult,
677
+ jestResult: makeJestResult({
678
+ numFailingTests: 1,
679
+ numPassingTests: 0,
680
+ }),
681
+ }))
682
+ .mockResolvedValueOnce(makeFileRunResult());
683
+ const session = makeSession({
684
+ config: {
685
+ metroPort: 8081,
686
+ resetEnvironmentBetweenTestFiles: false,
687
+ detectNativeCrashes: false,
688
+ runners: [
689
+ { platformId: 'android', name: 'android' },
690
+ { platformId: 'ios', name: 'ios' },
691
+ ],
692
+ } as HarnessSession['config'],
693
+ });
694
+
695
+ await executeRun(
696
+ session,
697
+ [makeTest('/a.ts'), makeTest('/b.ts')],
698
+ makeWatcher(),
699
+ makeEmitEvent().emitEvent,
700
+ makeGlobalConfig(),
701
+ );
702
+
703
+ expect(session.restartApp).toHaveBeenCalledTimes(1);
704
+ expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
705
+ });
706
+
707
+ it('restarts after a timeout in the last runnable file', async () => {
708
+ const timedOutResult = makeHarnessResult('failed');
709
+ timedOutResult.tests = [
710
+ {
711
+ name: 'hangs',
712
+ status: 'failed',
713
+ duration: 10,
714
+ error: {
715
+ name: 'TestCaseTimeoutError',
716
+ message: 'Test timed out after 10ms: hangs',
717
+ },
718
+ },
719
+ ];
720
+ mockRunHarnessTestFile.mockResolvedValueOnce(makeFileRunResult({
721
+ harnessResult: timedOutResult,
722
+ jestResult: makeJestResult({
723
+ numFailingTests: 1,
724
+ numPassingTests: 0,
725
+ }),
726
+ }));
727
+ const session = makeSession({
728
+ config: {
729
+ metroPort: 8081,
730
+ resetEnvironmentBetweenTestFiles: false,
731
+ detectNativeCrashes: false,
732
+ runners: [
733
+ { platformId: 'android', name: 'android' },
734
+ { platformId: 'ios', name: 'ios' },
735
+ ],
736
+ } as HarnessSession['config'],
737
+ });
738
+
739
+ await executeRun(
740
+ session,
741
+ [makeTest('/a.ts')],
742
+ makeWatcher(),
743
+ makeEmitEvent().emitEvent,
744
+ makeGlobalConfig({ watch: true }),
745
+ );
746
+
747
+ expect(session.restartApp).toHaveBeenCalledTimes(1);
748
+ expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
749
+ });
750
+
751
+ it('restarts after a suite hook timeout', async () => {
752
+ const timedOutResult = makeHarnessResult('failed');
753
+ timedOutResult.suites = [
754
+ {
755
+ name: 'suite',
756
+ tests: [],
757
+ suites: [],
758
+ status: 'failed',
759
+ duration: 10,
760
+ error: {
761
+ name: 'SuiteHookTimeoutError',
762
+ message: 'beforeAll hook timed out after 10ms in suite: suite',
763
+ },
764
+ },
765
+ ];
766
+ mockRunHarnessTestFile.mockResolvedValueOnce(makeFileRunResult({
767
+ harnessResult: timedOutResult,
768
+ jestResult: makeJestResult({
769
+ numFailingTests: 1,
770
+ numPassingTests: 0,
771
+ }),
772
+ }));
773
+ const session = makeSession();
774
+
775
+ await executeRun(
776
+ session,
777
+ [makeTest('/a.ts')],
778
+ makeWatcher(),
779
+ makeEmitEvent().emitEvent,
780
+ makeGlobalConfig(),
781
+ );
782
+
783
+ expect(session.restartApp).toHaveBeenCalledTimes(1);
784
+ expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
463
785
  });
464
786
  });
465
787
 
@@ -520,6 +842,7 @@ describe('executeRun', () => {
520
842
  );
521
843
 
522
844
  expect(session.restartApp).not.toHaveBeenCalled();
845
+ expect(session.resetEnvironment).not.toHaveBeenCalled();
523
846
  expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(1);
524
847
  });
525
848
  });
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { formatHarnessErrorMessage } from '../format-harness-error.js';
3
+
4
+ describe('formatHarnessErrorMessage', () => {
5
+ it('formats pending promise diagnostics for timeout errors', () => {
6
+ const message = formatHarnessErrorMessage(
7
+ {
8
+ name: 'TestCaseTimeoutError',
9
+ message: 'Test timed out after 50ms: suite hangs',
10
+ diagnostics: {
11
+ pendingPromises: {
12
+ total: 2,
13
+ items: [
14
+ {
15
+ id: 7,
16
+ createdAt: 110,
17
+ stack: 'Error: Promise created\n at hangs (example.ts:10:5)',
18
+ },
19
+ ],
20
+ },
21
+ },
22
+ },
23
+ { testStartedAt: 100 },
24
+ );
25
+
26
+ expect(message).toContain('Test timed out after 50ms: suite hangs');
27
+ expect(message).toContain('Pending promises at timeout: 2');
28
+ expect(message).toContain('Showing 1 of 2 pending promises.');
29
+ expect(message).toContain('Promise #7, created 10ms after test start:');
30
+ expect(message).toContain(' Error: Promise created');
31
+ });
32
+ });
@@ -0,0 +1,91 @@
1
+ import type { HarnessPlatform } from '@react-native-harness/platforms';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { maybeLogMetroCacheReused } from '../harness-session.js';
4
+
5
+ const { createHarnessCache, isWarm, logMetroCacheReused } = vi.hoisted(() => {
6
+ const isWarm = vi.fn();
7
+
8
+ return {
9
+ isWarm,
10
+ createHarnessCache: vi.fn((options: { projectRoot: string }) => ({
11
+ paths: {
12
+ root: `${options.projectRoot}/.harness/cache`,
13
+ metroTransform: `${options.projectRoot}/.harness/cache/metro`,
14
+ metroFileMap: `${options.projectRoot}/.harness/cache/metro-file-map`,
15
+ },
16
+ isWarm,
17
+ ensureDomainDirectories: vi.fn(),
18
+ })),
19
+ logMetroCacheReused: vi.fn(),
20
+ };
21
+ });
22
+
23
+ vi.mock('@react-native-harness/cache', () => ({
24
+ createHarnessCache,
25
+ }));
26
+
27
+ vi.mock('../logs.js', async (importOriginal) => ({
28
+ ...(await importOriginal<typeof import('../logs.js')>()),
29
+ logMetroCacheReused,
30
+ }));
31
+
32
+ const platform = { name: 'test-runner' } as HarnessPlatform;
33
+
34
+ describe('maybeLogMetroCacheReused', () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ });
38
+
39
+ it('logs the reuse message when caching is enabled and the cache is warm', () => {
40
+ isWarm.mockReturnValue(true);
41
+
42
+ maybeLogMetroCacheReused({
43
+ config: { cache: { metro: true } },
44
+ projectRoot: '/tmp/app',
45
+ platform,
46
+ });
47
+
48
+ expect(createHarnessCache).toHaveBeenCalledWith({
49
+ projectRoot: '/tmp/app',
50
+ });
51
+ expect(isWarm).toHaveBeenCalledWith('metro');
52
+ expect(logMetroCacheReused).toHaveBeenCalledWith(platform);
53
+ });
54
+
55
+ it('logs the reuse message with a warm cache when caching is on by default', () => {
56
+ isWarm.mockReturnValue(true);
57
+
58
+ maybeLogMetroCacheReused({
59
+ config: {},
60
+ projectRoot: '/tmp/app',
61
+ platform,
62
+ });
63
+
64
+ expect(logMetroCacheReused).toHaveBeenCalledWith(platform);
65
+ });
66
+
67
+ it('does not log when caching is enabled but the cache is cold', () => {
68
+ isWarm.mockReturnValue(false);
69
+
70
+ maybeLogMetroCacheReused({
71
+ config: { cache: { metro: true } },
72
+ projectRoot: '/tmp/app',
73
+ platform,
74
+ });
75
+
76
+ expect(logMetroCacheReused).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it('does not log when caching is disabled, regardless of cache state', () => {
80
+ isWarm.mockReturnValue(true);
81
+
82
+ maybeLogMetroCacheReused({
83
+ config: { cache: { metro: false } },
84
+ projectRoot: '/tmp/app',
85
+ platform,
86
+ });
87
+
88
+ expect(createHarnessCache).not.toHaveBeenCalled();
89
+ expect(logMetroCacheReused).not.toHaveBeenCalled();
90
+ });
91
+ });