rn-iso 0.1.0 → 0.2.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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -36
  3. package/bin/cli.js +4 -7
  4. package/package.json +28 -2
  5. package/skill/SKILL.md +41 -43
  6. package/src/commands/android.js +120 -14
  7. package/src/commands/ios.js +95 -33
  8. package/src/commands/release.js +19 -25
  9. package/src/commands/reserve.js +141 -144
  10. package/src/commands/status.js +1 -15
  11. package/src/commands/stop.js +62 -30
  12. package/src/commands/unreserve.js +23 -43
  13. package/src/config.js +14 -91
  14. package/src/labels.js +25 -0
  15. package/src/project.js +25 -7
  16. package/src/sim/android.js +31 -18
  17. package/src/sim/ios.js +7 -1
  18. package/.claude/settings.local.json +0 -7
  19. package/CLAUDE.md +0 -178
  20. package/docs/plans/2026-04-25-rn-iso-implementation.md +0 -2653
  21. package/docs/specs/2026-04-25-rn-iso-design.md +0 -282
  22. package/src/commands/logs.js +0 -28
  23. package/src/commands/prune.js +0 -57
  24. package/src/commands/shutdown.js +0 -41
  25. package/test/config.test.js +0 -208
  26. package/test/exec.test.js +0 -26
  27. package/test/fixtures/sample-bare-project/android/app/build.gradle +0 -6
  28. package/test/fixtures/sample-bare-project/ios/SampleApp.xcodeproj/project.pbxproj +0 -10
  29. package/test/fixtures/sample-bare-project/package.json +0 -4
  30. package/test/fixtures/sample-expo-project/app.json +0 -6
  31. package/test/fixtures/sample-expo-project/package.json +0 -4
  32. package/test/fixtures/sample-expo-project/src/.keep +0 -0
  33. package/test/metro.test.js +0 -34
  34. package/test/ports.test.js +0 -76
  35. package/test/project.test.js +0 -109
  36. package/test/runner.test.js +0 -209
  37. package/test/sim-android.test.js +0 -140
  38. package/test/sim-ios.test.js +0 -168
@@ -1,6 +0,0 @@
1
- {
2
- "expo": {
3
- "ios": { "bundleIdentifier": "com.example.sample" },
4
- "android": { "package": "com.example.sample" }
5
- }
6
- }
@@ -1,4 +0,0 @@
1
- {
2
- "name": "sample-app",
3
- "dependencies": { "expo": "~50.0.0" }
4
- }
File without changes
@@ -1,34 +0,0 @@
1
- import { test, afterEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { resetExecutor } from '../src/exec.js';
4
- import { logFileFor, projectHash, buildMetroSpawnArgs } from '../src/metro.js';
5
-
6
- afterEach(() => resetExecutor());
7
-
8
- test('projectHash is deterministic and short', () => {
9
- const a = projectHash('/foo/bar');
10
- const b = projectHash('/foo/bar');
11
- const c = projectHash('/foo/baz');
12
- assert.equal(a, b);
13
- assert.notEqual(a, c);
14
- assert.equal(a.length, 12);
15
- });
16
-
17
- test('logFileFor uses RN_ISO_HOME and project hash', () => {
18
- process.env.RN_ISO_HOME = '/tmp/test-rn-iso';
19
- const path = logFileFor('/some/project');
20
- assert.match(path, /^\/tmp\/test-rn-iso\/logs\/[0-9a-f]{12}\.log$/);
21
- delete process.env.RN_ISO_HOME;
22
- });
23
-
24
- test('buildMetroSpawnArgs returns correct argv for expo', () => {
25
- const { cmd, args } = buildMetroSpawnArgs({ isExpo: true, port: 8083 });
26
- assert.equal(cmd, 'npx');
27
- assert.deepEqual(args, ['expo', 'start', '--port', '8083']);
28
- });
29
-
30
- test('buildMetroSpawnArgs returns correct argv for bare', () => {
31
- const { cmd, args } = buildMetroSpawnArgs({ isExpo: false, port: 8083 });
32
- assert.equal(cmd, 'npx');
33
- assert.deepEqual(args, ['react-native', 'start', '--port', '8083']);
34
- });
@@ -1,76 +0,0 @@
1
- import { test, beforeEach, afterEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { mkdtempSync, rmSync } from 'fs';
4
- import { tmpdir } from 'os';
5
- import { join } from 'path';
6
- import { resetExecutor } from '../src/exec.js';
7
- import { upsertProject, setMetro } from '../src/config.js';
8
- import { computeNextPort, findReclaimablePort, allocatePort } from '../src/ports.js';
9
-
10
- let tmpHome;
11
-
12
- beforeEach(() => {
13
- tmpHome = mkdtempSync(join(tmpdir(), 'rn-iso-test-'));
14
- process.env.RN_ISO_HOME = tmpHome;
15
- });
16
-
17
- afterEach(() => {
18
- rmSync(tmpHome, { recursive: true, force: true });
19
- delete process.env.RN_ISO_HOME;
20
- resetExecutor();
21
- });
22
-
23
- test('computeNextPort returns 8082 with no existing ports', () => {
24
- assert.equal(computeNextPort(), 8082);
25
- });
26
-
27
- test('computeNextPort returns max + 1', () => {
28
- upsertProject('/a', { bundleId: 'a', androidPackage: 'a', isExpo: false });
29
- upsertProject('/b', { bundleId: 'b', androidPackage: 'b', isExpo: false });
30
- setMetro('/a', 8082, null);
31
- setMetro('/b', 8090, null);
32
- assert.equal(computeNextPort(), 8091);
33
- });
34
-
35
- test('findReclaimablePort returns null when no projects', async () => {
36
- const r = await findReclaimablePort('/excluded');
37
- assert.equal(r, null);
38
- });
39
-
40
- test('findReclaimablePort skips the excluded project path', async () => {
41
- upsertProject('/a', { bundleId: 'a', androidPackage: 'a', isExpo: false });
42
- setMetro('/a', 8082, null);
43
- // Mock isMetroRunning to always return false (port is dead)
44
- const r = await findReclaimablePort('/a', async () => false);
45
- assert.equal(r, null);
46
- });
47
-
48
- test('findReclaimablePort returns first dead port and its owner', async () => {
49
- upsertProject('/a', { bundleId: 'a', androidPackage: 'a', isExpo: false });
50
- upsertProject('/b', { bundleId: 'b', androidPackage: 'b', isExpo: false });
51
- setMetro('/a', 8082, null);
52
- setMetro('/b', 8083, null);
53
- // 8082 alive, 8083 dead
54
- const probe = async (port) => port === 8082;
55
- const r = await findReclaimablePort('/c', probe);
56
- assert.deepEqual(r, { port: 8083, ownerPath: '/b' });
57
- });
58
-
59
- test('allocatePort reclaims dead ports and removes the dead project', async () => {
60
- upsertProject('/a', { bundleId: 'a', androidPackage: 'a', isExpo: false });
61
- setMetro('/a', 8082, null);
62
- const probe = async () => false;
63
- const port = await allocatePort('/new', probe);
64
- assert.equal(port, 8082);
65
- // Caller should have removed /a -- verify via behavior
66
- const { getProject } = await import('../src/config.js');
67
- assert.equal(getProject('/a'), null);
68
- });
69
-
70
- test('allocatePort assigns a fresh port when nothing is reclaimable', async () => {
71
- upsertProject('/a', { bundleId: 'a', androidPackage: 'a', isExpo: false });
72
- setMetro('/a', 8082, null);
73
- const probe = async () => true; // everything alive
74
- const port = await allocatePort('/new', probe);
75
- assert.equal(port, 8083);
76
- });
@@ -1,109 +0,0 @@
1
- import { test, beforeEach, afterEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { mkdtempSync, rmSync } from 'fs';
4
- import { tmpdir } from 'os';
5
- import { resolve, join } from 'path';
6
- import { findProjectRoot, detectIsExpo, detectBundleId, detectAndroidPackage, resolveRegisteredProject } from '../src/project.js';
7
- import { upsertProject } from '../src/config.js';
8
-
9
- const FIXTURES = resolve(import.meta.dirname, 'fixtures');
10
- const EXPO_PROJ = join(FIXTURES, 'sample-expo-project');
11
- const BARE_PROJ = join(FIXTURES, 'sample-bare-project');
12
-
13
- test('findProjectRoot walks up from cwd to find package.json', () => {
14
- const nested = join(EXPO_PROJ, 'src');
15
- assert.equal(findProjectRoot(nested), EXPO_PROJ);
16
- });
17
-
18
- test('findProjectRoot returns null when no package.json found', () => {
19
- assert.equal(findProjectRoot('/'), null);
20
- });
21
-
22
- test('detectIsExpo true when expo deps + app.json has expo block', () => {
23
- assert.equal(detectIsExpo(EXPO_PROJ), true);
24
- });
25
-
26
- test('detectIsExpo false when expo is not in dependencies', () => {
27
- assert.equal(detectIsExpo(BARE_PROJ), false);
28
- });
29
-
30
- test('detectIsExpo trusts the ios script: react-native script wins even with expo dep', async () => {
31
- // Mimics rainbow: `expo` in deps for prebuild/modules, but the ios script
32
- // invokes react-native run-ios. Should NOT be flagged as Expo.
33
- const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = await import('fs');
34
- const { tmpdir } = await import('os');
35
- const tmp = mkdtempSync(join((await import('os')).tmpdir(), 'rn-iso-detect-'));
36
- try {
37
- writeFileSync(join(tmp, 'package.json'), JSON.stringify({
38
- dependencies: { expo: '54.0.33' },
39
- scripts: { ios: "react-native run-ios --simulator='iPhone 16 Pro'" },
40
- }));
41
- assert.equal(detectIsExpo(tmp), false);
42
- } finally {
43
- rmSync(tmp, { recursive: true, force: true });
44
- }
45
- });
46
-
47
- test('detectIsExpo trusts the ios script: expo run:ios wins', async () => {
48
- const { mkdtempSync, writeFileSync, rmSync } = await import('fs');
49
- const tmp = mkdtempSync(join((await import('os')).tmpdir(), 'rn-iso-detect-'));
50
- try {
51
- writeFileSync(join(tmp, 'package.json'), JSON.stringify({
52
- dependencies: { 'react-native': '0.74.0' },
53
- scripts: { ios: 'expo run:ios' },
54
- }));
55
- assert.equal(detectIsExpo(tmp), true);
56
- } finally {
57
- rmSync(tmp, { recursive: true, force: true });
58
- }
59
- });
60
-
61
- test('detectBundleId reads ios.bundleIdentifier from app.json', () => {
62
- assert.equal(detectBundleId(EXPO_PROJ), 'com.example.sample');
63
- });
64
-
65
- test('detectBundleId falls back to pbxproj when app config has no bundle id', () => {
66
- // BARE_PROJ has no app.json; the fixture pbxproj has main app id "me.sample"
67
- // alongside an extension target with a longer suffix. Picks the most-common
68
- // concrete value, tie-breaking by shortest length.
69
- assert.equal(detectBundleId(BARE_PROJ), 'me.sample');
70
- });
71
-
72
- test('detectAndroidPackage reads android.package from app.json', () => {
73
- assert.equal(detectAndroidPackage(EXPO_PROJ), 'com.example.sample');
74
- });
75
-
76
- test('detectAndroidPackage falls back to android/app/build.gradle (namespace)', () => {
77
- assert.equal(detectAndroidPackage(BARE_PROJ), 'me.sample');
78
- });
79
-
80
- // resolveRegisteredProject -- needs an isolated config home.
81
- let tmpHome;
82
- beforeEach(() => {
83
- tmpHome = mkdtempSync(join(tmpdir(), 'rn-iso-resolve-'));
84
- process.env.RN_ISO_HOME = tmpHome;
85
- });
86
- afterEach(() => {
87
- rmSync(tmpHome, { recursive: true, force: true });
88
- delete process.env.RN_ISO_HOME;
89
- });
90
-
91
- test('resolveRegisteredProject finds a project by absolute path', () => {
92
- upsertProject('/Users/x/Developer/agent-1', { bundleId: 'a', androidPackage: 'a', isExpo: false });
93
- const r = resolveRegisteredProject('/Users/x/Developer/agent-1');
94
- assert.equal(r.found, '/Users/x/Developer/agent-1');
95
- });
96
-
97
- test('resolveRegisteredProject does NOT do basename matching', () => {
98
- upsertProject('/Users/x/Developer/agent-1', { bundleId: 'a', androidPackage: 'a', isExpo: false });
99
- const r = resolveRegisteredProject('agent-1');
100
- assert.equal(r.found, null);
101
- assert.match(r.error, /Pass an absolute path/);
102
- });
103
-
104
- test('resolveRegisteredProject errors when path does not match anything', () => {
105
- upsertProject('/Users/x/Developer/agent-1', { bundleId: 'a', androidPackage: 'a', isExpo: false });
106
- const r = resolveRegisteredProject('/no/such/path');
107
- assert.equal(r.found, null);
108
- assert.match(r.error, /No registered project/);
109
- });
@@ -1,209 +0,0 @@
1
- import { test, beforeEach, afterEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs';
4
- import { tmpdir } from 'os';
5
- import { join } from 'path';
6
- import { setExecutor, resetExecutor } from '../src/exec.js';
7
- import {
8
- buildIosCommand,
9
- buildAndroidCommand,
10
- buildMetroCommand,
11
- buildScriptCommand,
12
- detectPackageManager,
13
- detectScriptCli,
14
- findLockfile,
15
- getProjectScript,
16
- resolveSimNameByUdid,
17
- } from '../src/runner.js';
18
-
19
- let tmpProj;
20
-
21
- function makeProj(files) {
22
- tmpProj = mkdtempSync(join(tmpdir(), 'rn-iso-runner-'));
23
- for (const [rel, content] of Object.entries(files)) {
24
- const dest = join(tmpProj, rel);
25
- mkdirSync(join(dest, '..'), { recursive: true });
26
- writeFileSync(dest, content);
27
- }
28
- return tmpProj;
29
- }
30
-
31
- afterEach(() => {
32
- if (tmpProj) rmSync(tmpProj, { recursive: true, force: true });
33
- tmpProj = null;
34
- resetExecutor();
35
- });
36
-
37
- // ---- Direct CLI fallback (no script) ----
38
-
39
- test('buildIosCommand expo fallback uses --device with UDID', () => {
40
- const root = makeProj({ 'package.json': '{}' });
41
- const cmd = buildIosCommand({ projectRoot: root, packageManager: 'npm', scriptName: 'ios', isExpo: true, udid: 'UDID-1', port: 8083, useScript: false });
42
- assert.equal(cmd, 'npx expo run:ios --device UDID-1 --port 8083');
43
- });
44
-
45
- test('buildIosCommand bare fallback uses --udid (not --simulator)', () => {
46
- const root = makeProj({ 'package.json': '{}' });
47
- const cmd = buildIosCommand({ projectRoot: root, packageManager: 'npm', scriptName: 'ios', isExpo: false, udid: 'UDID-1', port: 8083, useScript: false });
48
- assert.equal(cmd, 'npx react-native run-ios --udid UDID-1 --port 8083');
49
- });
50
-
51
- test('buildAndroidCommand expo fallback uses --device serial', () => {
52
- const root = makeProj({ 'package.json': '{}' });
53
- const cmd = buildAndroidCommand({ projectRoot: root, packageManager: 'npm', scriptName: 'android', isExpo: true, serial: 'emulator-5554', port: 8083, useScript: false });
54
- assert.equal(cmd, 'npx expo run:android --device emulator-5554 --port 8083');
55
- });
56
-
57
- test('buildAndroidCommand bare fallback uses --deviceId and RCT_METRO_PORT', () => {
58
- const root = makeProj({ 'package.json': '{}' });
59
- const cmd = buildAndroidCommand({ projectRoot: root, packageManager: 'npm', scriptName: 'android', isExpo: false, serial: 'emulator-5554', port: 8083, useScript: false });
60
- assert.equal(cmd, 'RCT_METRO_PORT=8083 npx react-native run-android --deviceId emulator-5554');
61
- });
62
-
63
- // ---- Script-based path ----
64
-
65
- test('buildIosCommand uses npm script with -- separator and --udid for RN script', () => {
66
- const root = makeProj({
67
- 'package.json': JSON.stringify({ scripts: { ios: 'react-native run-ios --simulator="iPhone 16 Pro"' } }),
68
- });
69
- const cmd = buildIosCommand({ projectRoot: root, packageManager: 'npm', scriptName: 'ios', isExpo: false, udid: 'UDID-1', port: 8083 });
70
- assert.equal(cmd, 'npm run ios -- --udid UDID-1 --port 8083');
71
- });
72
-
73
- test('buildIosCommand uses yarn script (no -- separator) and --udid for RN script', () => {
74
- const root = makeProj({
75
- 'package.json': JSON.stringify({ scripts: { ios: 'react-native run-ios' } }),
76
- });
77
- const cmd = buildIosCommand({ projectRoot: root, packageManager: 'yarn', scriptName: 'ios', isExpo: false, udid: 'UDID-1', port: 8083 });
78
- assert.equal(cmd, 'yarn ios --udid UDID-1 --port 8083');
79
- });
80
-
81
- test('buildIosCommand uses --device flag for expo script', () => {
82
- const root = makeProj({
83
- 'package.json': JSON.stringify({ scripts: { ios: 'expo run:ios' } }),
84
- });
85
- const cmd = buildIosCommand({ projectRoot: root, packageManager: 'pnpm', scriptName: 'ios', isExpo: true, udid: 'UDID-1', port: 8083 });
86
- assert.equal(cmd, 'pnpm ios --device UDID-1 --port 8083');
87
- });
88
-
89
- test('buildIosCommand falls back to direct CLI when script does not exist', () => {
90
- const root = makeProj({
91
- 'package.json': JSON.stringify({ scripts: { build: 'echo' } }),
92
- });
93
- const cmd = buildIosCommand({ projectRoot: root, packageManager: 'npm', scriptName: 'ios', isExpo: false, udid: 'UDID-1', port: 8083 });
94
- assert.equal(cmd, 'npx react-native run-ios --udid UDID-1 --port 8083');
95
- });
96
-
97
- test('buildAndroidCommand uses script via bun and --deviceId for RN script', () => {
98
- const root = makeProj({
99
- 'package.json': JSON.stringify({ scripts: { android: 'react-native run-android' } }),
100
- });
101
- const cmd = buildAndroidCommand({ projectRoot: root, packageManager: 'bun', scriptName: 'android', isExpo: false, serial: 'emulator-5554', port: 8083 });
102
- assert.equal(cmd, 'bun run android --deviceId emulator-5554 --port 8083');
103
- });
104
-
105
- // ---- Helpers ----
106
-
107
- test('detectPackageManager picks based on lockfile', () => {
108
- const yarnRoot = makeProj({ 'yarn.lock': '' });
109
- assert.equal(detectPackageManager(yarnRoot), 'yarn');
110
- rmSync(yarnRoot, { recursive: true });
111
-
112
- const pnpmRoot = makeProj({ 'pnpm-lock.yaml': '' });
113
- assert.equal(detectPackageManager(pnpmRoot), 'pnpm');
114
- rmSync(pnpmRoot, { recursive: true });
115
-
116
- const bunRoot = makeProj({ 'bun.lock': '' });
117
- assert.equal(detectPackageManager(bunRoot), 'bun');
118
- rmSync(bunRoot, { recursive: true });
119
-
120
- const npmRoot = makeProj({ 'package-lock.json': '' });
121
- assert.equal(detectPackageManager(npmRoot), 'npm');
122
- rmSync(npmRoot, { recursive: true });
123
-
124
- const noLock = makeProj({ 'package.json': '{}' });
125
- assert.equal(detectPackageManager(noLock), 'npm'); // default
126
- });
127
-
128
- test('detectPackageManager walks up to find lockfile in monorepo root', () => {
129
- // Layout:
130
- // /tmp/.../ <-- yarn.lock here (workspace root)
131
- // /tmp/.../apps/mobile/ <-- our "project" (no lockfile of its own)
132
- const root = makeProj({
133
- 'yarn.lock': '',
134
- 'package.json': JSON.stringify({ workspaces: ['apps/*'] }),
135
- 'apps/mobile/package.json': JSON.stringify({ name: 'mobile' }),
136
- });
137
- const projectRoot = join(root, 'apps/mobile');
138
- assert.equal(detectPackageManager(projectRoot), 'yarn');
139
- });
140
-
141
- test('findLockfile returns the lockfile dir and pm', () => {
142
- const root = makeProj({
143
- 'pnpm-lock.yaml': '',
144
- 'apps/mobile/package.json': '{}',
145
- });
146
- const found = findLockfile(join(root, 'apps/mobile'));
147
- assert.equal(found.pm, 'pnpm');
148
- assert.equal(found.dir, root);
149
- });
150
-
151
- test('findLockfile prefers nearest lockfile when nested ones exist', () => {
152
- // Some monorepos intentionally have nested lockfiles per package; pick the
153
- // closest one, not the workspace root's.
154
- const root = makeProj({
155
- 'yarn.lock': '',
156
- 'apps/mobile/pnpm-lock.yaml': '',
157
- 'apps/mobile/package.json': '{}',
158
- });
159
- const found = findLockfile(join(root, 'apps/mobile'));
160
- assert.equal(found.pm, 'pnpm');
161
- assert.equal(found.dir, join(root, 'apps/mobile'));
162
- });
163
-
164
- test('detectScriptCli identifies expo, react-native, or unknown', () => {
165
- assert.equal(detectScriptCli('expo run:ios'), 'expo');
166
- assert.equal(detectScriptCli('npx expo run:ios'), 'expo');
167
- assert.equal(detectScriptCli('react-native run-ios --simulator x'), 'react-native');
168
- assert.equal(detectScriptCli('npx react-native start'), 'react-native');
169
- assert.equal(detectScriptCli('echo hello'), 'unknown');
170
- assert.equal(detectScriptCli(''), 'unknown');
171
- });
172
-
173
- test('getProjectScript reads scripts from package.json', () => {
174
- const root = makeProj({
175
- 'package.json': JSON.stringify({ scripts: { ios: 'react-native run-ios' } }),
176
- });
177
- assert.equal(getProjectScript(root, 'ios'), 'react-native run-ios');
178
- assert.equal(getProjectScript(root, 'missing'), null);
179
- });
180
-
181
- test('buildScriptCommand uses the right convention per package manager', () => {
182
- assert.equal(buildScriptCommand('npm', 'ios', ['--udid X', '--port 8083']), 'npm run ios -- --udid X --port 8083');
183
- assert.equal(buildScriptCommand('yarn', 'ios', ['--udid X']), 'yarn ios --udid X');
184
- assert.equal(buildScriptCommand('pnpm', 'ios', ['--udid X']), 'pnpm ios --udid X');
185
- assert.equal(buildScriptCommand('bun', 'ios', ['--udid X']), 'bun run ios --udid X');
186
- assert.equal(buildScriptCommand('npm', 'ios', []), 'npm run ios');
187
- });
188
-
189
- // ---- Existing helpers retained ----
190
-
191
- test('buildMetroCommand picks expo or react-native', () => {
192
- assert.equal(buildMetroCommand({ isExpo: true, port: 8083 }), 'npx expo start --port 8083');
193
- assert.equal(buildMetroCommand({ isExpo: false, port: 8083 }), 'npx react-native start --port 8083');
194
- });
195
-
196
- test('resolveSimNameByUdid returns name from simctl JSON', () => {
197
- setExecutor({
198
- run: () => JSON.stringify({
199
- devices: {
200
- 'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
201
- { udid: 'UDID-1', name: 'iPhone 15', state: 'Booted', isAvailable: true },
202
- ],
203
- },
204
- }),
205
- runQuiet: () => null,
206
- spawn: () => null,
207
- });
208
- assert.equal(resolveSimNameByUdid('UDID-1'), 'iPhone 15');
209
- });
@@ -1,140 +0,0 @@
1
- import { test, beforeEach, afterEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { mkdtempSync, rmSync } from 'fs';
4
- import { tmpdir } from 'os';
5
- import { join } from 'path';
6
- import { setExecutor, resetExecutor } from '../src/exec.js';
7
- import {
8
- parseAvdList,
9
- parseAdbDevices,
10
- selectAndroidDevice,
11
- nextConsolePort,
12
- } from '../src/sim/android.js';
13
-
14
- let tmpHome;
15
-
16
- beforeEach(() => {
17
- tmpHome = mkdtempSync(join(tmpdir(), 'rn-iso-test-'));
18
- process.env.RN_ISO_HOME = tmpHome;
19
- });
20
-
21
- afterEach(() => {
22
- rmSync(tmpHome, { recursive: true, force: true });
23
- delete process.env.RN_ISO_HOME;
24
- resetExecutor();
25
- });
26
-
27
- test('parseAvdList strips header and blanks', () => {
28
- const out = `INFO | Storing AVDs in...\nPixel_6_API_34\nPixel_7_API_33\n`;
29
- const avds = parseAvdList(out);
30
- assert.deepEqual(avds, ['Pixel_6_API_34', 'Pixel_7_API_33']);
31
- });
32
-
33
- test('parseAdbDevices extracts running emulator console ports', () => {
34
- const out = `List of devices attached\nemulator-5554\tdevice\nemulator-5556\tdevice\n0123456789ABCDEF\tdevice\n`;
35
- const result = parseAdbDevices(out);
36
- assert.deepEqual(result.emulators.sort((a, b) => a.consolePort - b.consolePort), [
37
- { serial: 'emulator-5554', consolePort: 5554 },
38
- { serial: 'emulator-5556', consolePort: 5556 },
39
- ]);
40
- });
41
-
42
- test('parseAdbDevices ignores offline emulators', () => {
43
- const out = `List of devices attached\nemulator-5554\toffline\nemulator-5556\tdevice\n`;
44
- const result = parseAdbDevices(out);
45
- assert.deepEqual(result.emulators, [{ serial: 'emulator-5556', consolePort: 5556 }]);
46
- });
47
-
48
- test('nextConsolePort returns 5554 when none claimed', () => {
49
- assert.equal(nextConsolePort([]), 5554);
50
- });
51
-
52
- test('nextConsolePort returns next even port above max claimed', () => {
53
- assert.equal(nextConsolePort([5554, 5556]), 5558);
54
- });
55
-
56
- test('selectAndroidDevice prefers existing assignment when AVD still exists', () => {
57
- setExecutor({
58
- run: (cmd) => {
59
- if (cmd.includes('list-avds')) return 'Pixel_6_API_34\n';
60
- if (cmd.includes('adb devices')) return 'List of devices attached\n';
61
- throw new Error('unexpected: ' + cmd);
62
- },
63
- runQuiet: () => null,
64
- spawn: () => null,
65
- });
66
- const result = selectAndroidDevice({
67
- existingAvd: 'Pixel_6_API_34',
68
- existingConsolePort: 5554,
69
- claimedAvds: [],
70
- claimedConsolePorts: [],
71
- });
72
- assert.deepEqual(result, {
73
- kind: 'reuse',
74
- avdName: 'Pixel_6_API_34',
75
- consolePort: 5554,
76
- isRunning: false,
77
- });
78
- });
79
-
80
- test('selectAndroidDevice marks running when serial present in adb devices', () => {
81
- setExecutor({
82
- run: (cmd) => {
83
- if (cmd.includes('list-avds')) return 'Pixel_6_API_34\n';
84
- if (cmd.includes('adb devices')) return 'List of devices attached\nemulator-5554\tdevice\n';
85
- throw new Error('unexpected');
86
- },
87
- runQuiet: () => null,
88
- spawn: () => null,
89
- });
90
- const result = selectAndroidDevice({
91
- existingAvd: 'Pixel_6_API_34',
92
- existingConsolePort: 5554,
93
- claimedAvds: [],
94
- claimedConsolePorts: [],
95
- });
96
- assert.equal(result.isRunning, true);
97
- });
98
-
99
- test('selectAndroidDevice allocates first unclaimed AVD with next console port', () => {
100
- setExecutor({
101
- run: (cmd) => {
102
- if (cmd.includes('list-avds')) return 'Pixel_6_API_34\nPixel_7_API_33\n';
103
- if (cmd.includes('adb devices')) return 'List of devices attached\n';
104
- throw new Error('unexpected');
105
- },
106
- runQuiet: () => null,
107
- spawn: () => null,
108
- });
109
- const result = selectAndroidDevice({
110
- existingAvd: null,
111
- existingConsolePort: null,
112
- claimedAvds: ['Pixel_6_API_34'],
113
- claimedConsolePorts: [5554],
114
- });
115
- assert.deepEqual(result, {
116
- kind: 'allocate',
117
- avdName: 'Pixel_7_API_33',
118
- consolePort: 5556,
119
- isRunning: false,
120
- });
121
- });
122
-
123
- test('selectAndroidDevice returns noAvd when no AVDs exist', () => {
124
- setExecutor({
125
- run: (cmd) => {
126
- if (cmd.includes('list-avds')) return '';
127
- if (cmd.includes('adb devices')) return 'List of devices attached\n';
128
- throw new Error('unexpected');
129
- },
130
- runQuiet: () => null,
131
- spawn: () => null,
132
- });
133
- const result = selectAndroidDevice({
134
- existingAvd: null,
135
- existingConsolePort: null,
136
- claimedAvds: [],
137
- claimedConsolePorts: [],
138
- });
139
- assert.equal(result.kind, 'noAvd');
140
- });