Skip to content

Commit daec78e

Browse files
committed
feat(devicectl): Add install and launch commands
1 parent a299303 commit daec78e

2 files changed

Lines changed: 144 additions & 9 deletions

File tree

lib/devicectl.js

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ module.exports = {
6060
}
6161
},
6262

63-
list: function () {
64-
const result = spawnSync('xcrun', ['devicectl', 'list', 'devices', '--quiet', '--json-output', '/dev/stdout'], { encoding: 'utf8' });
63+
list: function (type = 'devices') {
64+
const result = spawnSync('xcrun', ['devicectl', 'list', type, '--quiet', '--json-output', '/dev/stdout'], { encoding: 'utf8' });
6565

6666
if (result.status === 0) {
6767
try {
@@ -72,5 +72,40 @@ module.exports = {
7272
}
7373

7474
return result;
75+
},
76+
77+
diagnose: function (deviceId) {
78+
const result = spawnSync('xcrun', ['devicectl', 'diagnose', '--device', deviceId, '--no-finder', '--quiet', '--json-output', '/dev/stdout'], { encoding: 'utf8' });
79+
80+
if (result.status === 0) {
81+
try {
82+
result.json = JSON.parse(result.stdout);
83+
} catch (err) {
84+
console.error(err.stack);
85+
}
86+
}
87+
88+
return result;
89+
},
90+
91+
install: function (deviceId, appPath) {
92+
return spawnSync('xcrun', ['devicectl', 'device', 'install', 'app', '--device', deviceId, appPath], { encoding: 'utf8' });
93+
},
94+
95+
launch: function (deviceId, bundleId, argv = [], options = {}) {
96+
const args = ['devicectl', 'device', 'process', 'launch', '--device', deviceId];
97+
98+
if (options.waitForDebugger || options.startStopped) {
99+
args.push('--start-stopped');
100+
}
101+
102+
if (options.console) {
103+
args.push('--console');
104+
}
105+
106+
args.push(bundleId);
107+
args.push(...argv);
108+
109+
return spawnSync('xcrun', args, { encoding: 'utf8' });
75110
}
76111
};

test/devicectl.spec.js

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ const spawnMock = test.mock.method(childProcess, 'spawnSync');
3030

3131
const devicectl = require('../lib/devicectl');
3232

33+
test.beforeEach(() => {
34+
spawnMock.mock.resetCalls();
35+
});
36+
3337
test('exports', (t) => {
3438
t.assert ||= require('node:assert');
3539

@@ -38,6 +42,9 @@ test('exports', (t) => {
3842
t.assert.equal(typeof devicectl.xcode_version, 'function');
3943
t.assert.equal(typeof devicectl.help, 'function');
4044
t.assert.equal(typeof devicectl.list, 'function');
45+
t.assert.equal(typeof devicectl.diagnose, 'function');
46+
t.assert.equal(typeof devicectl.install, 'function');
47+
t.assert.equal(typeof devicectl.launch, 'function');
4148
});
4249

4350
test('check_prerequisites fail', (t) => {
@@ -86,10 +93,6 @@ test('devicectl version', (t) => {
8693
});
8794

8895
test('devicectl help', async (ctx) => {
89-
ctx.beforeEach((t) => {
90-
spawnMock.mock.resetCalls();
91-
});
92-
9396
await ctx.test('with no arguments', (t) => {
9497
t.assert ||= require('node:assert');
9598

@@ -115,12 +118,10 @@ test('devicectl help', async (ctx) => {
115118

116119
test('devicectl list', async (ctx) => {
117120
ctx.beforeEach((t) => {
118-
spawnMock.mock.resetCalls();
119-
120121
t.mock.method(console, 'error', () => {});
121122
});
122123

123-
await ctx.test('with a successful response', (t) => {
124+
await ctx.test('with no arguments', (t) => {
124125
t.assert ||= require('node:assert');
125126

126127
spawnMock.mock.mockImplementationOnce(() => {
@@ -132,6 +133,18 @@ test('devicectl list', async (ctx) => {
132133
t.assert.deepEqual(retObj.json, { result: { devices: [] } });
133134
});
134135

136+
await ctx.test('with preferredDDI argument', (t) => {
137+
t.assert ||= require('node:assert');
138+
139+
spawnMock.mock.mockImplementationOnce(() => {
140+
return { status: 0, stdout: '{"result":{"platforms":[]}}' };
141+
});
142+
143+
const retObj = devicectl.list('preferredDDI');
144+
t.assert.deepEqual(spawnMock.mock.calls[0].arguments[1], ['devicectl', 'list', 'preferredDDI', '--quiet', '--json-output', '/dev/stdout']);
145+
t.assert.deepEqual(retObj.json, { result: { platforms: [] } });
146+
});
147+
135148
await ctx.test('with parsing error', (t) => {
136149
t.assert ||= require('node:assert');
137150

@@ -144,3 +157,90 @@ test('devicectl list', async (ctx) => {
144157
t.assert.equal(retObj.json, undefined);
145158
});
146159
});
160+
161+
test('devicectl diagnose', async (ctx) => {
162+
ctx.beforeEach((t) => {
163+
t.mock.method(console, 'error', () => {});
164+
});
165+
166+
await ctx.test('with a successful response', (t) => {
167+
t.assert ||= require('node:assert');
168+
169+
spawnMock.mock.mockImplementationOnce(() => {
170+
return { status: 0, stdout: '{"result":{}}' };
171+
});
172+
173+
const retObj = devicectl.diagnose('device_id');
174+
t.assert.deepEqual(spawnMock.mock.calls[0].arguments[1], ['devicectl', 'diagnose', '--device', 'device_id', '--no-finder', '--quiet', '--json-output', '/dev/stdout']);
175+
t.assert.deepEqual(retObj.json, { result: {} });
176+
});
177+
178+
await ctx.test('with parsing error', (t) => {
179+
t.assert ||= require('node:assert');
180+
181+
spawnMock.mock.mockImplementationOnce(() => {
182+
return { status: 0, stdout: 'This is not valid JSON' };
183+
});
184+
185+
const retObj = devicectl.diagnose('device_id');
186+
t.assert.match(console.error.mock.calls[0].arguments[0], /SyntaxError: Unexpected token/);
187+
t.assert.equal(retObj.json, undefined);
188+
});
189+
});
190+
191+
test('devicectl install', (t) => {
192+
t.assert ||= require('node:assert');
193+
194+
spawnMock.mock.mockImplementationOnce(() => {
195+
return { status: 0, stdout: '' };
196+
});
197+
198+
devicectl.install('device_id', 'path/to/bundle.app');
199+
t.assert.deepEqual(spawnMock.mock.calls[0].arguments[1], ['devicectl', 'device', 'install', 'app', '--device', 'device_id', 'path/to/bundle.app']);
200+
});
201+
202+
test('devicectl launch', async (ctx) => {
203+
await ctx.test('with no argv arguments', (t) => {
204+
t.assert ||= require('node:assert');
205+
206+
spawnMock.mock.mockImplementationOnce(() => {
207+
return { status: 0, stdout: '' };
208+
});
209+
210+
devicectl.launch('device_id', 'com.example.myapp');
211+
t.assert.deepEqual(spawnMock.mock.calls[0].arguments[1], ['devicectl', 'device', 'process', 'launch', '--device', 'device_id', 'com.example.myapp']);
212+
});
213+
214+
await ctx.test('with argv arguments', (t) => {
215+
t.assert ||= require('node:assert');
216+
217+
spawnMock.mock.mockImplementationOnce(() => {
218+
return { status: 0, stdout: '' };
219+
});
220+
221+
devicectl.launch('device_id', 'com.example.myapp', ['https://example.com']);
222+
t.assert.deepEqual(spawnMock.mock.calls[0].arguments[1], ['devicectl', 'device', 'process', 'launch', '--device', 'device_id', 'com.example.myapp', 'https://example.com']);
223+
});
224+
225+
await ctx.test('with startStopped option', (t) => {
226+
t.assert ||= require('node:assert');
227+
228+
spawnMock.mock.mockImplementationOnce(() => {
229+
return { status: 0, stdout: '' };
230+
});
231+
232+
devicectl.launch('device_id', 'com.example.myapp', [], { startStopped: true });
233+
t.assert.deepEqual(spawnMock.mock.calls[0].arguments[1], ['devicectl', 'device', 'process', 'launch', '--device', 'device_id', '--start-stopped', 'com.example.myapp']);
234+
});
235+
236+
await ctx.test('with console option', (t) => {
237+
t.assert ||= require('node:assert');
238+
239+
spawnMock.mock.mockImplementationOnce(() => {
240+
return { status: 0, stdout: '' };
241+
});
242+
243+
devicectl.launch('device_id', 'com.example.myapp', [], { console: true });
244+
t.assert.deepEqual(spawnMock.mock.calls[0].arguments[1], ['devicectl', 'device', 'process', 'launch', '--device', 'device_id', '--console', 'com.example.myapp']);
245+
});
246+
});

0 commit comments

Comments
 (0)