1- import { describe , expect , it , vi } from 'vitest' ;
1+ import { EventEmitter } from 'node:events' ;
2+ import { PassThrough } from 'node:stream' ;
3+ import { beforeEach , describe , expect , it , vi } from 'vitest' ;
24import {
35 createAvd ,
6+ emulatorProcess ,
47 getAppUid ,
58 getLogcatTimestamp ,
69 getStartAppArgs ,
710 hasAvd ,
811 installApp ,
12+ startEmulator ,
913 waitForBoot ,
1014 waitForEmulator ,
1115} from '../adb.js' ;
@@ -14,6 +18,24 @@ import * as tools from '@react-native-harness/tools';
1418const createAbortError = ( ) =>
1519 new DOMException ( 'The operation was aborted' , 'AbortError' ) ;
1620
21+ const createMockChildProcess = ( ) => {
22+ const process = new EventEmitter ( ) as EventEmitter & {
23+ stdout : PassThrough ;
24+ stderr : PassThrough ;
25+ unref : ReturnType < typeof vi . fn > ;
26+ } ;
27+
28+ process . stdout = new PassThrough ( ) ;
29+ process . stderr = new PassThrough ( ) ;
30+ process . unref = vi . fn ( ) ;
31+
32+ return process ;
33+ } ;
34+
35+ beforeEach ( ( ) => {
36+ vi . restoreAllMocks ( ) ;
37+ } ) ;
38+
1739describe ( 'getStartAppArgs' , ( ) => {
1840 it ( 'maps supported extras to adb am start flags' , ( ) => {
1941 expect (
@@ -66,7 +88,7 @@ describe('getStartAppArgs', () => {
6688 10234
6789 ) ;
6890
69- expect ( spawnSpy ) . toHaveBeenCalledWith ( ' adb' , [
91+ expect ( spawnSpy ) . toHaveBeenCalledWith ( expect . stringMatching ( / a d b $ / ) , [
7092 '-s' ,
7193 'emulator-5554' ,
7294 'shell' ,
@@ -86,7 +108,7 @@ describe('getStartAppArgs', () => {
86108 '03-12 11:35:08.000'
87109 ) ;
88110
89- expect ( spawnSpy ) . toHaveBeenCalledWith ( ' adb' , [
111+ expect ( spawnSpy ) . toHaveBeenCalledWith ( expect . stringMatching ( / a d b $ / ) , [
90112 '-s' ,
91113 'emulator-5554' ,
92114 'shell' ,
@@ -111,7 +133,7 @@ describe('getStartAppArgs', () => {
111133
112134 await installApp ( 'emulator-5554' , '/tmp/app.apk' ) ;
113135
114- expect ( spawnSpy ) . toHaveBeenCalledWith ( ' adb' , [
136+ expect ( spawnSpy ) . toHaveBeenCalledWith ( expect . stringMatching ( / a d b $ / ) , [
115137 '-s' ,
116138 'emulator-5554' ,
117139 'install' ,
@@ -133,19 +155,112 @@ describe('getStartAppArgs', () => {
133155 heapSize : '1G' ,
134156 } ) ;
135157
136- expect ( spawnSpy ) . toHaveBeenNthCalledWith ( 1 , 'sdkmanager' , [
137- 'system-images;android-35;default;x86_64' ,
138- ] ) ;
158+ expect ( spawnSpy ) . toHaveBeenNthCalledWith (
159+ 1 ,
160+ expect . stringMatching ( / s d k m a n a g e r $ / ) ,
161+ [ 'system-images;android-35;default;x86_64' ]
162+ ) ;
139163 expect ( spawnSpy ) . toHaveBeenNthCalledWith ( 2 , 'bash' , [
140164 '-lc' ,
141- `printf 'no\n' | avdmanager create avd --force --name "Pixel_8_API_35" --package "system-images;android-35;default;x86_64" --device "pixel_8"` ,
165+ expect . stringContaining (
166+ 'create avd --force --name "Pixel_8_API_35" --package "system-images;android-35;default;x86_64" --device "pixel_8"'
167+ ) ,
142168 ] ) ;
143169 expect ( spawnSpy ) . toHaveBeenNthCalledWith ( 3 , 'bash' , [
144170 '-lc' ,
145- `printf '%s\n%s\n' 'disk.dataPartition.size=1G' 'vm.heapSize=1G' >> "$HOME/.android/avd/Pixel_8_API_35.avd/config.ini"` ,
171+ expect . stringContaining (
172+ `'disk.dataPartition.size=1G' 'vm.heapSize=1G' >> `
173+ ) ,
146174 ] ) ;
147175 } ) ;
148176
177+ it ( 'surfaces emulator stdout when startup fails immediately' , async ( ) => {
178+ const child = createMockChildProcess ( ) ;
179+ let launcherReadyResolve : ( ( ) => void ) | undefined ;
180+ const launcherReady = new Promise < void > ( ( resolve ) => {
181+ launcherReadyResolve = resolve ;
182+ } ) ;
183+
184+ vi . spyOn ( tools , 'spawn' ) . mockResolvedValue ( {
185+ stdout : 'List of devices attached\n\n' ,
186+ } as Awaited < ReturnType < typeof tools . spawn > > ) ;
187+ vi . spyOn ( emulatorProcess , 'startDetachedProcess' ) . mockImplementation ( ( ) => {
188+ launcherReadyResolve ?.( ) ;
189+ return child as unknown as ReturnType <
190+ typeof emulatorProcess . startDetachedProcess
191+ > ;
192+ } ) ;
193+
194+ const startPromise = startEmulator ( 'Pixel_8_API_35' ) ;
195+ await launcherReady ;
196+
197+ child . stdout . write ( 'Unknown AVD name [Pixel_8_API_35]\n' ) ;
198+ child . stdout . end ( ) ;
199+ child . stderr . end ( ) ;
200+ child . emit ( 'close' , 1 , null ) ;
201+
202+ await expect ( startPromise ) . rejects . toThrow (
203+ 'Unknown AVD name [Pixel_8_API_35]'
204+ ) ;
205+ } ) ;
206+
207+ it ( 'surfaces emulator stderr when startup fails immediately' , async ( ) => {
208+ const child = createMockChildProcess ( ) ;
209+ let launcherReadyResolve : ( ( ) => void ) | undefined ;
210+ const launcherReady = new Promise < void > ( ( resolve ) => {
211+ launcherReadyResolve = resolve ;
212+ } ) ;
213+
214+ vi . spyOn ( tools , 'spawn' ) . mockResolvedValue ( {
215+ stdout : 'List of devices attached\n\n' ,
216+ } as Awaited < ReturnType < typeof tools . spawn > > ) ;
217+ vi . spyOn ( emulatorProcess , 'startDetachedProcess' ) . mockImplementation ( ( ) => {
218+ launcherReadyResolve ?.( ) ;
219+ return child as unknown as ReturnType <
220+ typeof emulatorProcess . startDetachedProcess
221+ > ;
222+ } ) ;
223+
224+ const startPromise = startEmulator ( 'Pixel_8_API_35' ) ;
225+ await launcherReady ;
226+
227+ child . stderr . write ( 'emulator: panic: broken config\n' ) ;
228+ child . stdout . end ( ) ;
229+ child . stderr . end ( ) ;
230+ child . emit ( 'close' , 1 , null ) ;
231+
232+ await expect ( startPromise ) . rejects . toThrow (
233+ 'emulator: panic: broken config'
234+ ) ;
235+ } ) ;
236+
237+ it ( 'returns after the emulator appears without waiting for process exit' , async ( ) => {
238+ vi . useFakeTimers ( ) ;
239+ const child = createMockChildProcess ( ) ;
240+ const spawnSpy = vi . spyOn ( tools , 'spawn' ) ;
241+
242+ spawnSpy
243+ . mockResolvedValueOnce ( {
244+ stdout : 'List of devices attached\nemulator-5554\tdevice\n' ,
245+ } as Awaited < ReturnType < typeof tools . spawn > > )
246+ . mockResolvedValueOnce ( {
247+ stdout : 'Pixel_8_API_35\n' ,
248+ } as Awaited < ReturnType < typeof tools . spawn > > ) ;
249+
250+ vi . spyOn ( emulatorProcess , 'startDetachedProcess' ) . mockReturnValue (
251+ child as unknown as ReturnType <
252+ typeof emulatorProcess . startDetachedProcess
253+ >
254+ ) ;
255+
256+ const startPromise = startEmulator ( 'Pixel_8_API_35' ) ;
257+
258+ await vi . runAllTimersAsync ( ) ;
259+
260+ await expect ( startPromise ) . resolves . toBeUndefined ( ) ;
261+ expect ( child . unref ) . toHaveBeenCalled ( ) ;
262+ } ) ;
263+
149264 it ( 'aborts while waiting for an emulator to appear' , async ( ) => {
150265 vi . useFakeTimers ( ) ;
151266 vi . spyOn ( tools , 'spawn' ) . mockResolvedValue ( {
0 commit comments