11import { beforeEach , describe , expect , it , vi } from 'vitest' ;
2+ import fs from 'node:fs' ;
3+ import os from 'node:os' ;
4+ import path from 'node:path' ;
25import {
6+ ensureAndroidAdbAvailable ,
7+ ensureAndroidEmulatorAvailable ,
38 getAndroidSdkRoot ,
49 getAndroidSystemImagePackage ,
510 getDefaultUnixAndroidSdkRoot ,
611 getHostAndroidSystemImageArch ,
712 getRequiredAndroidSdkPackages ,
813} from '../environment.js' ;
14+ import * as tools from '@react-native-harness/tools' ;
915
1016describe ( 'Android environment' , ( ) => {
1117 beforeEach ( ( ) => {
1218 vi . restoreAllMocks ( ) ;
1319 vi . unstubAllEnvs ( ) ;
1420 } ) ;
1521
22+ it ( 'skips bootstrapping command-line tools when adb is already installed' , async ( ) => {
23+ const sdkRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'android-sdk-' ) ) ;
24+ const adbPath = path . join ( sdkRoot , 'platform-tools' , 'adb' ) ;
25+
26+ fs . mkdirSync ( path . dirname ( adbPath ) , { recursive : true } ) ;
27+ fs . writeFileSync ( adbPath , '' ) ;
28+
29+ const spawnSpy = vi . spyOn ( tools , 'spawn' ) ;
30+
31+ await expect (
32+ ensureAndroidAdbAvailable ( {
33+ env : { ANDROID_HOME : sdkRoot } ,
34+ } ) ,
35+ ) . resolves . toBe ( sdkRoot ) ;
36+
37+ expect ( spawnSpy ) . not . toHaveBeenCalled ( ) ;
38+
39+ fs . rmSync ( sdkRoot , { force : true , recursive : true } ) ;
40+ } ) ;
41+
42+ it ( 'installs only platform-tools when adb is missing' , async ( ) => {
43+ const sdkRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'android-sdk-' ) ) ;
44+ const sdkManagerDirectory = path . join (
45+ sdkRoot ,
46+ 'cmdline-tools' ,
47+ 'latest' ,
48+ 'bin' ,
49+ ) ;
50+
51+ fs . mkdirSync ( sdkManagerDirectory , { recursive : true } ) ;
52+ fs . writeFileSync ( path . join ( sdkManagerDirectory , 'sdkmanager' ) , '' ) ;
53+ fs . writeFileSync ( path . join ( sdkManagerDirectory , 'avdmanager' ) , '' ) ;
54+
55+ const spawnSpy = vi . spyOn ( tools , 'spawn' ) . mockImplementation ( ( async (
56+ command : string ,
57+ args ?: readonly string [ ] ,
58+ ) => {
59+ if ( command === 'bash' && typeof args ?. [ 1 ] === 'string' ) {
60+ const commandString = args [ 1 ] ;
61+
62+ if ( commandString . includes ( 'platform-tools' ) ) {
63+ const adbPath = path . join ( sdkRoot , 'platform-tools' , 'adb' ) ;
64+ fs . mkdirSync ( path . dirname ( adbPath ) , { recursive : true } ) ;
65+ fs . writeFileSync ( adbPath , '' ) ;
66+ }
67+ }
68+
69+ return { } as Awaited < ReturnType < typeof tools . spawn > > ;
70+ } ) as typeof tools . spawn ) ;
71+
72+ await expect (
73+ ensureAndroidAdbAvailable ( {
74+ env : { ANDROID_HOME : sdkRoot } ,
75+ } ) ,
76+ ) . resolves . toBe ( sdkRoot ) ;
77+
78+ expect ( spawnSpy ) . toHaveBeenCalledWith (
79+ 'bash' ,
80+ [ '-lc' , expect . stringContaining ( 'platform-tools' ) ] ,
81+ expect . any ( Object ) ,
82+ ) ;
83+ expect (
84+ spawnSpy . mock . calls . some (
85+ ( [ command , args ] ) =>
86+ command === 'bash' &&
87+ typeof args ?. [ 1 ] === 'string' &&
88+ args [ 1 ] . includes ( 'platform-tools' ) &&
89+ args [ 1 ] . includes ( 'emulator' ) ,
90+ ) ,
91+ ) . toBe ( false ) ;
92+
93+ fs . rmSync ( sdkRoot , { force : true , recursive : true } ) ;
94+ } ) ;
95+
96+ it ( 'installs emulator only when emulator is missing' , async ( ) => {
97+ const sdkRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'android-sdk-' ) ) ;
98+ const sdkManagerDirectory = path . join (
99+ sdkRoot ,
100+ 'cmdline-tools' ,
101+ 'latest' ,
102+ 'bin' ,
103+ ) ;
104+
105+ fs . mkdirSync ( sdkManagerDirectory , { recursive : true } ) ;
106+ fs . writeFileSync ( path . join ( sdkManagerDirectory , 'sdkmanager' ) , '' ) ;
107+ fs . writeFileSync ( path . join ( sdkManagerDirectory , 'avdmanager' ) , '' ) ;
108+
109+ const spawnSpy = vi . spyOn ( tools , 'spawn' ) . mockImplementation ( ( async (
110+ command : string ,
111+ args ?: readonly string [ ] ,
112+ ) => {
113+ if ( command === 'bash' && typeof args ?. [ 1 ] === 'string' ) {
114+ const commandString = args [ 1 ] ;
115+
116+ if ( commandString . includes ( 'emulator' ) ) {
117+ const emulatorPath = path . join ( sdkRoot , 'emulator' , 'emulator' ) ;
118+ fs . mkdirSync ( path . dirname ( emulatorPath ) , { recursive : true } ) ;
119+ fs . writeFileSync ( emulatorPath , '' ) ;
120+ }
121+ }
122+
123+ return { } as Awaited < ReturnType < typeof tools . spawn > > ;
124+ } ) as typeof tools . spawn ) ;
125+
126+ await expect (
127+ ensureAndroidEmulatorAvailable ( {
128+ env : { ANDROID_HOME : sdkRoot } ,
129+ } ) ,
130+ ) . resolves . toBe ( sdkRoot ) ;
131+
132+ expect ( spawnSpy ) . toHaveBeenCalledWith (
133+ 'bash' ,
134+ [ '-lc' , expect . stringContaining ( 'emulator' ) ] ,
135+ expect . any ( Object ) ,
136+ ) ;
137+
138+ fs . rmSync ( sdkRoot , { force : true , recursive : true } ) ;
139+ } ) ;
140+
16141 it ( 'uses the default Unix SDK root when env vars are missing' , ( ) => {
17142 expect (
18143 getDefaultUnixAndroidSdkRoot ( {
19144 platform : 'darwin' ,
20145 homeDirectory : '/Users/tester' ,
21- } )
146+ } ) ,
22147 ) . toBe ( '/Users/tester/Library/Android/sdk' ) ;
23148
24149 expect (
@@ -27,8 +152,8 @@ describe('Android environment', () => {
27152 {
28153 platform : 'linux' ,
29154 homeDirectory : '/home/tester' ,
30- }
31- )
155+ } ,
156+ ) ,
32157 ) . toBe ( '/home/tester/Android/Sdk' ) ;
33158 } ) ;
34159
@@ -42,8 +167,8 @@ describe('Android environment', () => {
42167 {
43168 platform : 'darwin' ,
44169 homeDirectory : '/Users/tester' ,
45- }
46- )
170+ } ,
171+ ) ,
47172 ) . toBe ( '/env/android-home' ) ;
48173
49174 expect (
@@ -54,19 +179,19 @@ describe('Android environment', () => {
54179 {
55180 platform : 'linux' ,
56181 homeDirectory : '/home/tester' ,
57- }
58- )
182+ } ,
183+ ) ,
59184 ) . toBe ( '/env/android-sdk-root' ) ;
60185 } ) ;
61186
62187 it ( 'selects Android packages using the host architecture' , ( ) => {
63188 expect ( getHostAndroidSystemImageArch ( 'x64' ) ) . toBe ( 'x86_64' ) ;
64189 expect ( getHostAndroidSystemImageArch ( 'arm64' ) ) . toBe ( 'arm64-v8a' ) ;
65190 expect ( getAndroidSystemImagePackage ( 35 , 'x86_64' ) ) . toBe (
66- 'system-images;android-35;default;x86_64'
191+ 'system-images;android-35;default;x86_64' ,
67192 ) ;
68193 expect ( getAndroidSystemImagePackage ( 35 , 'arm64-v8a' ) ) . toBe (
69- 'system-images;android-35;default;arm64-v8a'
194+ 'system-images;android-35;default;arm64-v8a' ,
70195 ) ;
71196 } ) ;
72197
@@ -76,7 +201,7 @@ describe('Android environment', () => {
76201 apiLevel : 34 ,
77202 includeEmulator : true ,
78203 architecture : 'x86_64' ,
79- } )
204+ } ) ,
80205 ) . toEqual ( [
81206 'platform-tools' ,
82207 'emulator' ,
0 commit comments