@react-native-harness/platform-android 1.2.0 → 1.4.0-rc.1

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 (41) hide show
  1. package/dist/__tests__/app-monitor.test.js +180 -102
  2. package/dist/__tests__/crash-diagnostics.test.d.ts +2 -0
  3. package/dist/__tests__/crash-diagnostics.test.d.ts.map +1 -0
  4. package/dist/__tests__/crash-diagnostics.test.js +119 -0
  5. package/dist/__tests__/crash-reporter.test.d.ts +2 -0
  6. package/dist/__tests__/crash-reporter.test.d.ts.map +1 -0
  7. package/dist/__tests__/crash-reporter.test.js +150 -0
  8. package/dist/__tests__/instance.test.js +133 -24
  9. package/dist/adb.d.ts +4 -0
  10. package/dist/adb.d.ts.map +1 -1
  11. package/dist/adb.js +42 -2
  12. package/dist/app-session.d.ts +17 -0
  13. package/dist/app-session.d.ts.map +1 -0
  14. package/dist/app-session.js +209 -0
  15. package/dist/crash-diagnostics.d.ts +40 -0
  16. package/dist/crash-diagnostics.d.ts.map +1 -0
  17. package/dist/crash-diagnostics.js +256 -0
  18. package/dist/crash-reporter.d.ts +12 -0
  19. package/dist/crash-reporter.d.ts.map +1 -0
  20. package/dist/crash-reporter.js +150 -0
  21. package/dist/device-state.d.ts +6 -0
  22. package/dist/device-state.d.ts.map +1 -0
  23. package/dist/device-state.js +142 -0
  24. package/dist/instance.d.ts +1 -1
  25. package/dist/instance.d.ts.map +1 -1
  26. package/dist/instance.js +32 -59
  27. package/dist/runner.d.ts.map +1 -1
  28. package/dist/runner.js +1 -1
  29. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  30. package/package.json +4 -4
  31. package/src/__tests__/crash-diagnostics.test.ts +144 -0
  32. package/src/__tests__/crash-reporter.test.ts +167 -0
  33. package/src/__tests__/instance.test.ts +174 -31
  34. package/src/adb.ts +61 -2
  35. package/src/app-session.ts +300 -0
  36. package/src/crash-diagnostics.ts +471 -0
  37. package/src/crash-reporter.ts +263 -0
  38. package/src/instance.ts +56 -91
  39. package/src/runner.ts +7 -3
  40. package/test/setup.ts +1 -0
  41. package/src/app-monitor.ts +0 -577
@@ -0,0 +1,144 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ collectDropboxArtifacts,
4
+ filterExitInfo,
5
+ getBestDropboxArtifact,
6
+ parseDropboxOutput,
7
+ } from '../crash-diagnostics.js';
8
+
9
+ const javaCrashDropbox = `
10
+ Drop box contents: 1 entries
11
+ Max entries: 1000
12
+ ========================================
13
+ 2026-03-12 10:30:45.123 data_app_crash (text, 500 bytes)
14
+ Process: com.harnessplayground
15
+ PID: 7777
16
+ Package: com.harnessplayground v1 (1.0)
17
+ java.lang.RuntimeException: boom
18
+ \tat com.harnessplayground.MainActivity.onCreate(MainActivity.kt:38)
19
+ `;
20
+
21
+ const nativeCrashDropbox = `
22
+ ========================================
23
+ 2026-03-12 10:31:10.456 data_app_native_crash (text, 800 bytes)
24
+ Process: com.harnessplayground
25
+ PID: 8888
26
+ signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xdeadbeef
27
+ Abort message: 'JNI DETECTED ERROR IN APPLICATION'
28
+ backtrace:
29
+ #00 pc 00001234 /data/app/lib/libapp.so
30
+ `;
31
+
32
+ const deviceJavaCrashDropbox = `
33
+ Drop box contents: 8 entries
34
+ Searching for: data_app_crash
35
+
36
+ ========================================
37
+ 2026-05-29 10:22:34 data_app_crash (text, 1328 bytes)
38
+ SystemUptimeMs: 79467172
39
+ Process: com.harnessplayground
40
+ PID: 586
41
+ Package: com.harnessplayground v1 (1.0)
42
+ java.lang.RuntimeException: Intentional asynchronous Kotlin crash
43
+ \tat com.harnessplayground.PlaygroundCrashModule.crashFromKotlinAsync(PlaygroundCrashModule.kt:44)
44
+ `;
45
+
46
+ describe('collectDropboxArtifacts', () => {
47
+ it('matches harness playground crashes from merged per-tag dropbox output', async () => {
48
+ const artifacts = await collectDropboxArtifacts({
49
+ bundleId: 'com.harnessplayground',
50
+ getDropboxOutput: async () => deviceJavaCrashDropbox,
51
+ occurredAt: Date.now(),
52
+ pid: 586,
53
+ });
54
+
55
+ expect(artifacts).toHaveLength(1);
56
+ expect(artifacts[0]).toMatchObject({
57
+ artifactType: 'dropbox-crash',
58
+ pid: 586,
59
+ processName: 'com.harnessplayground',
60
+ });
61
+ });
62
+
63
+ it('ignores Dropbox entries older than the session start time', async () => {
64
+ const artifacts = await collectDropboxArtifacts({
65
+ bundleId: 'com.harnessplayground',
66
+ getDropboxOutput: async () => deviceJavaCrashDropbox,
67
+ minOccurredAt: Date.parse('2026-05-29T10:22:35.000Z'),
68
+ occurredAt: Date.parse('2026-05-29T10:22:36.000Z'),
69
+ });
70
+
71
+ expect(artifacts).toHaveLength(0);
72
+ });
73
+ });
74
+
75
+ describe('parseDropboxOutput', () => {
76
+ it('parses java and native dropbox entries', () => {
77
+ const entries = parseDropboxOutput(`${javaCrashDropbox}\n${nativeCrashDropbox}`);
78
+
79
+ expect(entries).toHaveLength(2);
80
+ expect(entries[0]).toMatchObject({
81
+ tag: 'data_app_crash',
82
+ pid: 7777,
83
+ });
84
+ expect(entries[1]).toMatchObject({
85
+ tag: 'data_app_native_crash',
86
+ pid: 8888,
87
+ });
88
+ expect(entries[1]?.content).toContain('SIGSEGV');
89
+ });
90
+ });
91
+
92
+ describe('getBestDropboxArtifact', () => {
93
+ it('prefers the highest-scored artifact', () => {
94
+ const entries = parseDropboxOutput(`${javaCrashDropbox}\n${nativeCrashDropbox}`);
95
+ const artifacts = entries.map((entry, index) => ({
96
+ artifactType:
97
+ entry.tag === 'data_app_native_crash'
98
+ ? ('dropbox-native-crash' as const)
99
+ : ('dropbox-crash' as const),
100
+ dropboxTag: entry.tag,
101
+ occurredAt: Date.now(),
102
+ score: index === 0 ? 100 : 300,
103
+ summary: entry.content,
104
+ pid: entry.pid,
105
+ processName: 'com.harnessplayground',
106
+ }));
107
+
108
+ expect(getBestDropboxArtifact(artifacts)?.dropboxTag).toBe(
109
+ 'data_app_native_crash'
110
+ );
111
+ });
112
+ });
113
+
114
+ describe('filterExitInfo', () => {
115
+ it('returns matching exit-info records for the target package and pid', () => {
116
+ const output = `
117
+ ApplicationExitInfo #0:
118
+ package=com.harnessplayground pid=7777 realUid=10123
119
+ reason=4 (APP CRASH(NATIVE))
120
+ timestamp=2026-03-12 10:31:10.456
121
+ ApplicationExitInfo #1:
122
+ package=com.other.app pid=9999 realUid=10199
123
+ reason=10 (USER REQUESTED)
124
+ `;
125
+
126
+ const filtered = filterExitInfo({
127
+ output,
128
+ bundleId: 'com.harnessplayground',
129
+ pid: 7777,
130
+ });
131
+
132
+ expect(filtered).toContain('com.harnessplayground pid=7777');
133
+ expect(filtered).not.toContain('com.other.app');
134
+ });
135
+
136
+ it('returns null when no exit-info records exist', () => {
137
+ expect(
138
+ filterExitInfo({
139
+ output: 'No exit info records for com.harnessplayground',
140
+ bundleId: 'com.harnessplayground',
141
+ })
142
+ ).toBeNull();
143
+ });
144
+ });
@@ -0,0 +1,167 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { createAndroidCrashReporter } from '../crash-reporter.js';
3
+
4
+ const javaCrashDropbox = `
5
+ ========================================
6
+ 2026-03-12 10:30:45.123 data_app_crash (text, 500 bytes)
7
+ Process: com.harnessplayground
8
+ PID: 7777
9
+ Package: com.harnessplayground v1 (1.0)
10
+ java.lang.RuntimeException: boom
11
+ \tat com.harnessplayground.MainActivity.onCreate(MainActivity.kt:38)
12
+ `;
13
+
14
+ const nativeCrashDropbox = `
15
+ ========================================
16
+ 2026-03-12 10:31:10.456 data_app_native_crash (text, 800 bytes)
17
+ Process: com.harnessplayground
18
+ PID: 8888
19
+ signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xdeadbeef
20
+ Abort message: 'JNI DETECTED ERROR IN APPLICATION'
21
+ backtrace:
22
+ #00 pc 00001234 /data/app/lib/libapp.so
23
+ `;
24
+
25
+ describe('createAndroidCrashReporter', () => {
26
+ afterEach(() => {
27
+ vi.useRealTimers();
28
+ });
29
+
30
+ it('persists the current logcat crash block with the test file path', async () => {
31
+ vi.useFakeTimers();
32
+ const persistArtifact = vi.fn(
33
+ () => '/tmp/.harness/crash-reports/logcat.txt'
34
+ );
35
+ const reporter = createAndroidCrashReporter({
36
+ bundleId: 'com.harnessplayground',
37
+ crashArtifactWriter: {
38
+ runTimestamp: '2026-03-12T11-35-08-000Z',
39
+ persistArtifact,
40
+ },
41
+ getLogs: () => [
42
+ { line: 'ordinary crash-buffer line', occurredAt: 1_000 },
43
+ { line: '--------- beginning of crash', occurredAt: 1_100 },
44
+ {
45
+ line: 'Process: com.harnessplayground, PID: 7777',
46
+ occurredAt: 1_100,
47
+ },
48
+ { line: 'FATAL EXCEPTION: main', occurredAt: 1_100 },
49
+ ],
50
+ });
51
+
52
+ const detailsPromise = reporter.getCrashDetails({
53
+ occurredAt: 1_100,
54
+ pid: 7777,
55
+ testFilePath: '/test/native-crash.test.ts',
56
+ });
57
+ await vi.advanceTimersByTimeAsync(100);
58
+ const details = await detailsPromise;
59
+
60
+ expect(details).toMatchObject({
61
+ artifactType: 'logcat',
62
+ artifactPath: '/tmp/.harness/crash-reports/logcat.txt',
63
+ pid: 7777,
64
+ processName: 'com.harnessplayground',
65
+ });
66
+ expect(persistArtifact).toHaveBeenCalledWith({
67
+ artifactKind: 'logcat',
68
+ testFilePath: '/test/native-crash.test.ts',
69
+ source: {
70
+ kind: 'text',
71
+ fileName: 'logcat.txt',
72
+ text: [
73
+ '--------- beginning of crash',
74
+ 'Process: com.harnessplayground, PID: 7777',
75
+ 'FATAL EXCEPTION: main',
76
+ '',
77
+ ].join('\n'),
78
+ },
79
+ });
80
+ });
81
+
82
+ it('adds dropbox artifacts as enrichment when logcat evidence is available', async () => {
83
+ vi.useFakeTimers();
84
+ const persistArtifact = vi
85
+ .fn()
86
+ .mockImplementation(({ artifactKind }: { artifactKind: string }) => {
87
+ return `/tmp/.harness/crash-reports/${artifactKind}.txt`;
88
+ });
89
+ const reporter = createAndroidCrashReporter({
90
+ bundleId: 'com.harnessplayground',
91
+ crashArtifactWriter: {
92
+ runTimestamp: '2026-03-12T11-35-08-000Z',
93
+ persistArtifact,
94
+ },
95
+ getLogs: () => [
96
+ { line: '--------- beginning of crash', occurredAt: 1_100 },
97
+ {
98
+ line: 'Process: com.harnessplayground, PID: 7777',
99
+ occurredAt: 1_100,
100
+ },
101
+ { line: 'FATAL EXCEPTION: main', occurredAt: 1_100 },
102
+ ],
103
+ getDropboxOutput: async () => javaCrashDropbox,
104
+ getExitInfo: async () =>
105
+ 'ApplicationExitInfo #0:\n package=com.harnessplayground pid=7777 reason=4 (APP CRASH(NATIVE))',
106
+ });
107
+
108
+ const detailsPromise = reporter.getCrashDetails({
109
+ occurredAt: 1_100,
110
+ pid: 7777,
111
+ testFilePath: '/test/native-crash.test.ts',
112
+ });
113
+ await vi.advanceTimersByTimeAsync(100);
114
+ const details = await detailsPromise;
115
+
116
+ expect(details).toMatchObject({
117
+ artifactType: 'logcat',
118
+ artifactPath: '/tmp/.harness/crash-reports/logcat.txt',
119
+ });
120
+ expect(details?.enrichmentArtifacts).toEqual(
121
+ expect.arrayContaining([
122
+ {
123
+ artifactType: 'dropbox-crash',
124
+ artifactPath: '/tmp/.harness/crash-reports/dropbox-crash.txt',
125
+ },
126
+ {
127
+ artifactType: 'exit-info',
128
+ artifactPath: '/tmp/.harness/crash-reports/exit-info.txt',
129
+ },
130
+ ])
131
+ );
132
+ });
133
+
134
+ it('falls back to native dropbox evidence when logcat is empty', async () => {
135
+ vi.useFakeTimers();
136
+ const persistArtifact = vi
137
+ .fn()
138
+ .mockImplementation(({ artifactKind }: { artifactKind: string }) => {
139
+ return `/tmp/.harness/crash-reports/${artifactKind}.txt`;
140
+ });
141
+ const reporter = createAndroidCrashReporter({
142
+ bundleId: 'com.harnessplayground',
143
+ crashArtifactWriter: {
144
+ runTimestamp: '2026-03-12T11-35-08-000Z',
145
+ persistArtifact,
146
+ },
147
+ getLogs: () => [],
148
+ getDropboxOutput: async () => nativeCrashDropbox,
149
+ });
150
+
151
+ const detailsPromise = reporter.getCrashDetails({
152
+ occurredAt: 1_100,
153
+ pid: 8888,
154
+ testFilePath: '/test/native-crash.test.ts',
155
+ });
156
+ await vi.advanceTimersByTimeAsync(100);
157
+ const details = await detailsPromise;
158
+
159
+ expect(details).toMatchObject({
160
+ artifactType: 'dropbox-native-crash',
161
+ artifactPath: '/tmp/.harness/crash-reports/dropbox-native-crash.txt',
162
+ pid: 8888,
163
+ signal: 'SIGSEGV',
164
+ processName: 'com.harnessplayground',
165
+ });
166
+ });
167
+ });
@@ -3,6 +3,7 @@ import {
3
3
  DEFAULT_METRO_PORT,
4
4
  type Config as HarnessConfig,
5
5
  } from '@react-native-harness/config';
6
+ import type { Subprocess } from '@react-native-harness/tools';
6
7
  import {
7
8
  getAndroidEmulatorPlatformInstance,
8
9
  getAndroidPhysicalDevicePlatformInstance,
@@ -12,6 +13,21 @@ import * as avdConfig from '../avd-config.js';
12
13
  import * as sharedPrefs from '../shared-prefs.js';
13
14
  import { HarnessAppPathError, HarnessEmulatorConfigError } from '../errors.js';
14
15
 
16
+ const createLogcatProcess = (lines: string[] = []): Subprocess => {
17
+ const process = {
18
+ nodeChildProcess: Promise.resolve({
19
+ kill: vi.fn(),
20
+ }),
21
+ [Symbol.asyncIterator]: async function* () {
22
+ for (const line of lines) {
23
+ yield line;
24
+ }
25
+ },
26
+ };
27
+
28
+ return process as unknown as Subprocess;
29
+ };
30
+
15
31
  const harnessConfig = {
16
32
  metroPort: DEFAULT_METRO_PORT,
17
33
  } as HarnessConfig;
@@ -27,6 +43,7 @@ describe('Android platform instance', () => {
27
43
  beforeEach(async () => {
28
44
  vi.restoreAllMocks();
29
45
  vi.unstubAllEnvs();
46
+ vi.useRealTimers();
30
47
  vi.spyOn(
31
48
  await import('../environment.js'),
32
49
  'ensureAndroidEmulatorAvailable',
@@ -47,6 +64,9 @@ describe('Android platform instance', () => {
47
64
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
48
65
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
49
66
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
67
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue(
68
+ '01-01 00:00:00.000',
69
+ );
50
70
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
51
71
  undefined,
52
72
  );
@@ -98,6 +118,9 @@ describe('Android platform instance', () => {
98
118
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
99
119
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
100
120
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
121
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue(
122
+ '01-01 00:00:00.000',
123
+ );
101
124
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
102
125
  undefined,
103
126
  );
@@ -159,6 +182,9 @@ describe('Android platform instance', () => {
159
182
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
160
183
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
161
184
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
185
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue(
186
+ '01-01 00:00:00.000',
187
+ );
162
188
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
163
189
  undefined,
164
190
  );
@@ -215,6 +241,9 @@ describe('Android platform instance', () => {
215
241
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
216
242
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
217
243
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
244
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue(
245
+ '01-01 00:00:00.000',
246
+ );
218
247
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
219
248
  undefined,
220
249
  );
@@ -276,6 +305,9 @@ describe('Android platform instance', () => {
276
305
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
277
306
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
278
307
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
308
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue(
309
+ '01-01 00:00:00.000',
310
+ );
279
311
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
280
312
  undefined,
281
313
  );
@@ -343,6 +375,9 @@ describe('Android platform instance', () => {
343
375
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
344
376
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
345
377
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
378
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue(
379
+ '01-01 00:00:00.000',
380
+ );
346
381
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
347
382
  undefined,
348
383
  );
@@ -466,7 +501,8 @@ describe('Android platform instance', () => {
466
501
  ).rejects.toBeInstanceOf(HarnessEmulatorConfigError);
467
502
  });
468
503
 
469
- it('returns a noop emulator app monitor when native crash detection is disabled', async () => {
504
+ it('creates a real emulator app session when native crash detection is disabled', async () => {
505
+ vi.useFakeTimers();
470
506
  vi.spyOn(
471
507
  await import('../environment.js'),
472
508
  'ensureAndroidEmulatorEnvironment',
@@ -478,9 +514,18 @@ describe('Android platform instance', () => {
478
514
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
479
515
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
480
516
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
517
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue(
518
+ '01-01 00:00:00.000',
519
+ );
481
520
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
482
521
  undefined,
483
522
  );
523
+ const stopApp = vi.spyOn(adb, 'stopApp').mockResolvedValue(undefined);
524
+ const startApp = vi.spyOn(adb, 'startApp').mockResolvedValue(undefined);
525
+ const getAppPid = vi.spyOn(adb, 'getAppPid').mockResolvedValue(4321);
526
+ const startLogcat = vi
527
+ .spyOn(adb, 'startLogcat')
528
+ .mockReturnValue(createLogcatProcess());
484
529
 
485
530
  const instance = await getAndroidEmulatorPlatformInstance(
486
531
  {
@@ -503,16 +548,107 @@ describe('Android platform instance', () => {
503
548
  );
504
549
 
505
550
  const listener = vi.fn();
506
- const appMonitor = instance.createAppMonitor();
551
+ const appSession = await instance.createAppSession();
552
+ appSession.addListener(listener);
553
+ await vi.advanceTimersByTimeAsync(0);
554
+
555
+ expect(startLogcat).toHaveBeenCalledWith('emulator-5554', [
556
+ 'logcat',
557
+ '-v',
558
+ 'threadtime',
559
+ '-b',
560
+ 'crash',
561
+ '--uid=10234',
562
+ '-T',
563
+ '01-01 00:00:00.000',
564
+ ]);
565
+ expect(startLogcat.mock.invocationCallOrder[0]).toBeLessThan(
566
+ startApp.mock.invocationCallOrder[0],
567
+ );
568
+ await expect(appSession.getState()).resolves.toEqual({
569
+ status: 'running',
570
+ pid: 4321,
571
+ });
572
+ expect(getAppPid).toHaveBeenCalled();
573
+ expect(listener).not.toHaveBeenCalled();
574
+ expect(appSession.removeListener(listener)).toBeUndefined();
575
+ await expect(appSession.dispose()).resolves.toBeUndefined();
576
+ expect(stopApp).toHaveBeenCalled();
577
+ });
578
+
579
+ it('reports an early Android crash from logcat before a PID poll succeeds', async () => {
580
+ vi.useFakeTimers();
581
+ vi.spyOn(
582
+ await import('../environment.js'),
583
+ 'ensureAndroidEmulatorEnvironment',
584
+ ).mockResolvedValue('/tmp/android-sdk');
585
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
586
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
587
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
588
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
589
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
590
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
591
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
592
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue(
593
+ '01-01 00:00:00.000',
594
+ );
595
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
596
+ undefined,
597
+ );
598
+ vi.spyOn(adb, 'stopApp').mockResolvedValue(undefined);
599
+ vi.spyOn(adb, 'startApp').mockResolvedValue(undefined);
600
+ vi.spyOn(adb, 'getAppPid').mockResolvedValue(null);
601
+ vi.spyOn(adb, 'startLogcat').mockReturnValue(
602
+ createLogcatProcess([
603
+ '--------- beginning of crash',
604
+ 'Process: com.harnessplayground, PID: 7777',
605
+ 'FATAL EXCEPTION: main',
606
+ ]),
607
+ );
507
608
 
508
- await expect(appMonitor.start()).resolves.toBeUndefined();
509
- await expect(appMonitor.stop()).resolves.toBeUndefined();
510
- await expect(appMonitor.dispose()).resolves.toBeUndefined();
511
- expect(appMonitor.addListener(listener)).toBeUndefined();
512
- expect(appMonitor.removeListener(listener)).toBeUndefined();
609
+ const instance = await getAndroidEmulatorPlatformInstance(
610
+ {
611
+ name: 'android',
612
+ device: {
613
+ type: 'emulator',
614
+ name: 'Pixel_8_API_35',
615
+ avd: {
616
+ apiLevel: 35,
617
+ profile: 'pixel_8',
618
+ diskSize: '1G',
619
+ heapSize: '1G',
620
+ },
621
+ },
622
+ bundleId: 'com.harnessplayground',
623
+ activityName: '.MainActivity',
624
+ },
625
+ harnessConfig,
626
+ init,
627
+ );
628
+
629
+ const listener = vi.fn();
630
+ const appSession = await instance.createAppSession();
631
+ appSession.addListener(listener);
632
+
633
+ await vi.advanceTimersByTimeAsync(0);
634
+
635
+ expect(appSession.getLogs().map((entry) => entry.line)).toEqual([
636
+ '--------- beginning of crash',
637
+ 'Process: com.harnessplayground, PID: 7777',
638
+ 'FATAL EXCEPTION: main',
639
+ ]);
640
+ await expect(appSession.getState()).resolves.toMatchObject({
641
+ status: 'exited',
642
+ pid: 7777,
643
+ reason: 'observed-exit',
644
+ });
645
+ expect(listener).toHaveBeenCalledWith({ type: 'app_exited' });
646
+
647
+ await appSession.dispose();
513
648
  });
514
649
 
515
- it('returns a noop physical device app monitor when native crash detection is disabled', async () => {
650
+ it('creates a real physical-device app session when native crash detection is disabled', async () => {
651
+ vi.useFakeTimers();
516
652
  vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['012345']);
517
653
  vi.spyOn(adb, 'getDeviceInfo').mockResolvedValue({
518
654
  manufacturer: 'motorola',
@@ -522,25 +658,18 @@ describe('Android platform instance', () => {
522
658
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
523
659
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
524
660
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
661
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue(
662
+ '01-01 00:00:00.000',
663
+ );
525
664
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
526
665
  undefined,
527
666
  );
528
-
529
- await expect(
530
- getAndroidPhysicalDevicePlatformInstance(
531
- {
532
- name: 'android-device',
533
- device: {
534
- type: 'physical',
535
- manufacturer: 'motorola',
536
- model: 'moto g72',
537
- },
538
- bundleId: 'com.harnessplayground',
539
- activityName: '.MainActivity',
540
- },
541
- harnessConfigWithoutNativeCrashDetection,
542
- ),
543
- ).resolves.toBeDefined();
667
+ vi.spyOn(adb, 'stopApp').mockResolvedValue(undefined);
668
+ vi.spyOn(adb, 'startApp').mockResolvedValue(undefined);
669
+ vi.spyOn(adb, 'getAppPid').mockResolvedValue(8765);
670
+ const startLogcat = vi
671
+ .spyOn(adb, 'startLogcat')
672
+ .mockReturnValue(createLogcatProcess());
544
673
 
545
674
  const instance = await getAndroidPhysicalDevicePlatformInstance(
546
675
  {
@@ -557,13 +686,27 @@ describe('Android platform instance', () => {
557
686
  );
558
687
 
559
688
  const listener = vi.fn();
560
- const appMonitor = instance.createAppMonitor();
561
-
562
- await expect(appMonitor.start()).resolves.toBeUndefined();
563
- await expect(appMonitor.stop()).resolves.toBeUndefined();
564
- await expect(appMonitor.dispose()).resolves.toBeUndefined();
565
- expect(appMonitor.addListener(listener)).toBeUndefined();
566
- expect(appMonitor.removeListener(listener)).toBeUndefined();
689
+ const appSession = await instance.createAppSession();
690
+ appSession.addListener(listener);
691
+ await vi.advanceTimersByTimeAsync(0);
692
+
693
+ expect(startLogcat).toHaveBeenCalledWith('012345', [
694
+ 'logcat',
695
+ '-v',
696
+ 'threadtime',
697
+ '-b',
698
+ 'crash',
699
+ '--uid=10234',
700
+ '-T',
701
+ '01-01 00:00:00.000',
702
+ ]);
703
+ await expect(appSession.getState()).resolves.toEqual({
704
+ status: 'running',
705
+ pid: 8765,
706
+ });
707
+ expect(listener).not.toHaveBeenCalled();
708
+ expect(appSession.removeListener(listener)).toBeUndefined();
709
+ await expect(appSession.dispose()).resolves.toBeUndefined();
567
710
  });
568
711
 
569
712
  it('grants permissions when permissions are enabled for emulator', async () => {