@houwert/conductor 0.32.1 → 0.33.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.
- package/dist/commands/fold.js +69 -0
- package/dist/commands/get-orientation.js +40 -0
- package/dist/commands/screenshot.js +54 -2
- package/dist/commands/set-orientation.js +59 -10
- package/dist/drivers/bootstrap.js +13 -0
- package/dist/drivers/devicectl.js +52 -0
- package/dist/drivers/ios-displays.js +51 -0
- package/dist/drivers/ios-fold.js +209 -0
- package/dist/enum-options.js +32 -2
- package/dist/index.js +19 -0
- package/package.json +1 -1
- package/skills/conductor-device-setup/SKILL.md +30 -1
- package/skills/conductor-inspect/SKILL.md +1 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP = void 0;
|
|
4
|
+
exports.getFold = getFold;
|
|
5
|
+
exports.setFold = setFold;
|
|
6
|
+
exports.HELP = ` get-fold Print the hinge angle of a foldable device (iPhone Duo)
|
|
7
|
+
set-fold <closed|book|open|0-180> Fold, half-open, unfold, or set an exact hinge angle`;
|
|
8
|
+
const runner_js_1 = require("../runner.js");
|
|
9
|
+
const session_js_1 = require("../session.js");
|
|
10
|
+
const output_js_1 = require("../output.js");
|
|
11
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
12
|
+
const ios_fold_js_1 = require("../drivers/ios-fold.js");
|
|
13
|
+
async function resolveDeviceId(sessionName) {
|
|
14
|
+
if (sessionName !== 'default')
|
|
15
|
+
return sessionName;
|
|
16
|
+
const session = await (0, session_js_1.getSession)(sessionName);
|
|
17
|
+
return session.deviceId ?? (await (0, runner_js_1.detectFirstDevice)());
|
|
18
|
+
}
|
|
19
|
+
/** Fold control talks to the device directly, so no test driver is needed. */
|
|
20
|
+
async function foldableDeviceId(sessionName, opts) {
|
|
21
|
+
const deviceId = await resolveDeviceId(sessionName);
|
|
22
|
+
if (!deviceId) {
|
|
23
|
+
(0, output_js_1.printError)('No device found. Connect a device or start a simulator first.', opts);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
|
|
27
|
+
if (platform !== 'ios') {
|
|
28
|
+
(0, output_js_1.printError)(`fold control is iOS-only — foldable simulators such as the iPhone Duo`, opts);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return deviceId;
|
|
32
|
+
}
|
|
33
|
+
async function getFold(opts = {}, sessionName = 'default') {
|
|
34
|
+
const deviceId = await foldableDeviceId(sessionName, opts);
|
|
35
|
+
if (!deviceId)
|
|
36
|
+
return 1;
|
|
37
|
+
try {
|
|
38
|
+
const state = await (0, ios_fold_js_1.getFoldState)(deviceId);
|
|
39
|
+
if (opts.json)
|
|
40
|
+
(0, output_js_1.printData)(state, opts);
|
|
41
|
+
else
|
|
42
|
+
(0, output_js_1.printSuccess)(`fold: ${state.pose} (${state.angle}°)`, opts);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
(0, output_js_1.printError)(`get-fold — failed\n${err instanceof Error ? err.message : String(err)}`, opts);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function setFold(value, opts = {}, sessionName = 'default') {
|
|
51
|
+
const key = value.trim().toLowerCase();
|
|
52
|
+
const angle = (0, ios_fold_js_1.resolveFoldAngle)(value);
|
|
53
|
+
if (angle === null) {
|
|
54
|
+
(0, output_js_1.printError)(`set-fold must be one of: ${Object.keys(ios_fold_js_1.FOLD_POSES).join(', ')}, or an angle from 0 to 180`, opts);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
const deviceId = await foldableDeviceId(sessionName, opts);
|
|
58
|
+
if (!deviceId)
|
|
59
|
+
return 1;
|
|
60
|
+
try {
|
|
61
|
+
await (0, ios_fold_js_1.setFoldAngle)(deviceId, angle);
|
|
62
|
+
(0, output_js_1.printSuccess)(`set-fold ${key in ios_fold_js_1.FOLD_POSES ? key : angle} (${angle}°) — done`, opts);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
(0, output_js_1.printError)(`set-fold ${value} — failed\n${err instanceof Error ? err.message : String(err)}`, opts);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP = void 0;
|
|
4
|
+
exports.getOrientation = getOrientation;
|
|
5
|
+
exports.HELP = ` get-orientation Print the device's current orientation`;
|
|
6
|
+
const runner_js_1 = require("../runner.js");
|
|
7
|
+
const session_js_1 = require("../session.js");
|
|
8
|
+
const output_js_1 = require("../output.js");
|
|
9
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
10
|
+
const devicectl_js_1 = require("../drivers/devicectl.js");
|
|
11
|
+
async function resolveDeviceId(sessionName) {
|
|
12
|
+
if (sessionName !== 'default')
|
|
13
|
+
return sessionName;
|
|
14
|
+
const session = await (0, session_js_1.getSession)(sessionName);
|
|
15
|
+
return session.deviceId ?? (await (0, runner_js_1.detectFirstDevice)());
|
|
16
|
+
}
|
|
17
|
+
async function getOrientation(opts = {}, sessionName = 'default') {
|
|
18
|
+
const deviceId = await resolveDeviceId(sessionName);
|
|
19
|
+
if (!deviceId) {
|
|
20
|
+
(0, output_js_1.printError)('No device found. Connect a device or start a simulator first.', opts);
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
|
|
24
|
+
if (platform !== 'ios') {
|
|
25
|
+
(0, output_js_1.printError)('get-orientation is iOS-only (simulators and physical devices)', opts);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const orientation = await (0, devicectl_js_1.getOrientation)(deviceId);
|
|
30
|
+
if (opts.json)
|
|
31
|
+
(0, output_js_1.printData)({ orientation }, opts);
|
|
32
|
+
else
|
|
33
|
+
(0, output_js_1.printSuccess)(`orientation: ${orientation}`, opts);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
(0, output_js_1.printError)(`get-orientation — failed\n${err instanceof Error ? err.message : String(err)}`, opts);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -5,8 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.HELP = void 0;
|
|
7
7
|
exports.screenshot = screenshot;
|
|
8
|
-
exports.HELP = ` take-screenshot [<element>] [--output <path>] [--full-page]
|
|
8
|
+
exports.HELP = ` take-screenshot [<element>] [--output <path>] [--full-page] [--display <panel>]
|
|
9
9
|
Take screenshot (--full-page: web only, capture entire scrollable page)
|
|
10
|
+
--display <cover|inner|id> Which display to capture (default: whichever panel
|
|
11
|
+
is live). An unknown value lists the device's displays
|
|
10
12
|
<element> Crop to the element matched by text (positional)
|
|
11
13
|
--id <id> Crop to the element matched by accessibility id
|
|
12
14
|
--text <text> Crop to the element matched by text only (not id)
|
|
@@ -21,9 +23,14 @@ exports.HELP = ` take-screenshot [<element>] [--output <path>] [--full-page]
|
|
|
21
23
|
--left-of <text> Match element left of the given reference
|
|
22
24
|
--right-of <text> Match element right of the given reference`;
|
|
23
25
|
const path_1 = __importDefault(require("path"));
|
|
26
|
+
const os_1 = __importDefault(require("os"));
|
|
24
27
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
28
|
+
const child_process_1 = require("child_process");
|
|
29
|
+
const util_1 = require("util");
|
|
25
30
|
const runner_js_1 = require("../runner.js");
|
|
26
31
|
const output_js_1 = require("../output.js");
|
|
32
|
+
const devicectl_js_1 = require("../drivers/devicectl.js");
|
|
33
|
+
const ios_displays_js_1 = require("../drivers/ios-displays.js");
|
|
27
34
|
const ios_js_1 = require("../drivers/ios.js");
|
|
28
35
|
const android_js_1 = require("../drivers/android.js");
|
|
29
36
|
const web_js_1 = require("../drivers/web.js");
|
|
@@ -33,6 +40,51 @@ const wait_js_1 = require("../drivers/wait.js");
|
|
|
33
40
|
const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
|
|
34
41
|
const png_crop_js_1 = require("../png-crop.js");
|
|
35
42
|
const DEFAULT_MARGIN_PX = 8;
|
|
43
|
+
const exec = (0, util_1.promisify)(child_process_1.execFile);
|
|
44
|
+
/**
|
|
45
|
+
* Grab the screen, pointing at the right panel on a multi-display device.
|
|
46
|
+
*
|
|
47
|
+
* The driver always screenshots `XCUIScreen.main`, which on a foldable is the
|
|
48
|
+
* cover panel — powered off, and so a black image, whenever the device is
|
|
49
|
+
* unfolded. When the device reports more than one integrated panel we capture
|
|
50
|
+
* the live one through simctl instead. Ordinary devices keep the driver path.
|
|
51
|
+
*/
|
|
52
|
+
async function captureScreen(driver, opts, displayOverride) {
|
|
53
|
+
const deviceId = driver instanceof ios_js_1.IOSDriver ? driver.deviceId : undefined;
|
|
54
|
+
if (!deviceId || (!displayOverride && !(driver instanceof ios_js_1.IOSDriver))) {
|
|
55
|
+
return await driver.screenshot(opts);
|
|
56
|
+
}
|
|
57
|
+
const displays = await (0, devicectl_js_1.listDisplays)(deviceId).catch(() => []);
|
|
58
|
+
if (!displays.length) {
|
|
59
|
+
if (displayOverride)
|
|
60
|
+
throw new Error("could not read this device's displays");
|
|
61
|
+
return await driver.screenshot(opts);
|
|
62
|
+
}
|
|
63
|
+
const choice = (0, ios_displays_js_1.pickCaptureDisplay)(displays, displayOverride);
|
|
64
|
+
if (choice.error)
|
|
65
|
+
throw new Error(choice.error);
|
|
66
|
+
const primary = displays.find((d) => d.primary);
|
|
67
|
+
// Nothing to redirect: the driver already captures the primary panel.
|
|
68
|
+
if (choice.displayId === null || (primary && choice.displayId === primary.displayId)) {
|
|
69
|
+
return await driver.screenshot(opts);
|
|
70
|
+
}
|
|
71
|
+
const file = path_1.default.join(os_1.default.tmpdir(), `conductor-shot-${Date.now()}.png`);
|
|
72
|
+
try {
|
|
73
|
+
await exec('xcrun', [
|
|
74
|
+
'simctl',
|
|
75
|
+
'io',
|
|
76
|
+
deviceId,
|
|
77
|
+
'screenshot',
|
|
78
|
+
'--display',
|
|
79
|
+
String(choice.displayId),
|
|
80
|
+
file,
|
|
81
|
+
]);
|
|
82
|
+
return await promises_1.default.readFile(file);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await promises_1.default.unlink(file).catch(() => { });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
36
88
|
async function screenshot(outputPath, opts = {}, sessionName = 'default', fullPage = false, query = '', flags = {}) {
|
|
37
89
|
const timestamp = Date.now();
|
|
38
90
|
const defaultName = `screenshot-${timestamp}.png`;
|
|
@@ -63,7 +115,7 @@ async function screenshot(outputPath, opts = {}, sessionName = 'default', fullPa
|
|
|
63
115
|
: '';
|
|
64
116
|
const margin = flags.margin ?? DEFAULT_MARGIN_PX;
|
|
65
117
|
const result = await (0, runner_js_1.runDirect)(async (driver) => {
|
|
66
|
-
const buf = await driver
|
|
118
|
+
const buf = await captureScreen(driver, { fullPage }, flags.display);
|
|
67
119
|
let out = buf;
|
|
68
120
|
if (sel) {
|
|
69
121
|
let el;
|
|
@@ -2,24 +2,73 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HELP = void 0;
|
|
4
4
|
exports.setOrientation = setOrientation;
|
|
5
|
-
exports.HELP = ` set-orientation <
|
|
5
|
+
exports.HELP = ` set-orientation <orientation> Set device orientation
|
|
6
|
+
portrait | landscape (all platforms)
|
|
7
|
+
portraitUpsideDown | landscapeLeft | landscapeRight |
|
|
8
|
+
faceUp | faceDown (iOS only)`;
|
|
6
9
|
const runner_js_1 = require("../runner.js");
|
|
10
|
+
const session_js_1 = require("../session.js");
|
|
7
11
|
const output_js_1 = require("../output.js");
|
|
8
|
-
const
|
|
12
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
13
|
+
const devicectl_js_1 = require("../drivers/devicectl.js");
|
|
14
|
+
const ios_fold_js_1 = require("../drivers/ios-fold.js");
|
|
15
|
+
/** Accepted everywhere; other platforms handle these through their driver. */
|
|
16
|
+
const BASIC = {
|
|
17
|
+
portrait: 'portrait',
|
|
18
|
+
landscape: 'landscapeLeft',
|
|
19
|
+
};
|
|
20
|
+
/** iOS-only orientations, which the test driver has no concept of. */
|
|
21
|
+
const IOS_ONLY = {
|
|
22
|
+
portraitupsidedown: 'portraitUpsideDown',
|
|
23
|
+
landscapeleft: 'landscapeLeft',
|
|
24
|
+
landscaperight: 'landscapeRight',
|
|
25
|
+
faceup: 'faceUp',
|
|
26
|
+
facedown: 'faceDown',
|
|
27
|
+
};
|
|
9
28
|
async function setOrientation(orientation, opts = {}, sessionName = 'default') {
|
|
10
|
-
|
|
11
|
-
|
|
29
|
+
const key = orientation.toLowerCase();
|
|
30
|
+
const iosValue = BASIC[key] ?? IOS_ONLY[key];
|
|
31
|
+
if (!iosValue) {
|
|
32
|
+
(0, output_js_1.printError)(`set-orientation must be one of: ${[...Object.keys(BASIC), ...Object.values(IOS_ONLY)].join(', ')}`, opts);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
const deviceId = sessionName !== 'default'
|
|
36
|
+
? sessionName
|
|
37
|
+
: ((await (0, session_js_1.getSession)(sessionName)).deviceId ?? (await (0, runner_js_1.detectFirstDevice)()));
|
|
38
|
+
if (!deviceId) {
|
|
39
|
+
(0, output_js_1.printError)('No device found. Connect a device or start a simulator first.', opts);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
|
|
43
|
+
// On iOS everything goes through devicectl: it covers the full set and needs
|
|
44
|
+
// no test driver running. Other platforms keep using their driver.
|
|
45
|
+
if (platform === 'ios') {
|
|
46
|
+
try {
|
|
47
|
+
await (0, devicectl_js_1.setOrientation)(deviceId, iosValue);
|
|
48
|
+
// Foldables accept the call and ignore it — their pose machine owns
|
|
49
|
+
// orientation — so confirm, and rotate through the relay if it didn't take.
|
|
50
|
+
if ((await (0, devicectl_js_1.getOrientation)(deviceId).catch(() => null)) !== iosValue) {
|
|
51
|
+
await (0, ios_fold_js_1.setOrientationViaRelay)(deviceId, iosValue);
|
|
52
|
+
}
|
|
53
|
+
(0, output_js_1.printSuccess)(`set-orientation ${iosValue} — done`, opts);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
(0, output_js_1.printError)(`set-orientation ${orientation} — failed\n${err instanceof Error ? err.message : String(err)}`, opts);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!BASIC[key]) {
|
|
62
|
+
(0, output_js_1.printError)(`${iosValue} is iOS-only — use portrait or landscape on this platform`, opts);
|
|
12
63
|
return 1;
|
|
13
64
|
}
|
|
14
65
|
const result = await (0, runner_js_1.runDirect)(async (driver) => {
|
|
15
|
-
await driver.setOrientation(
|
|
66
|
+
await driver.setOrientation(key);
|
|
16
67
|
}, sessionName);
|
|
17
68
|
if (result.success) {
|
|
18
|
-
(0, output_js_1.printSuccess)(`set-orientation ${
|
|
69
|
+
(0, output_js_1.printSuccess)(`set-orientation ${key} — done`, opts);
|
|
19
70
|
return 0;
|
|
20
71
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return 1;
|
|
24
|
-
}
|
|
72
|
+
(0, output_js_1.printError)(`set-orientation ${orientation} — failed\n${result.stderr}`, opts);
|
|
73
|
+
return 1;
|
|
25
74
|
}
|
|
@@ -11,6 +11,7 @@ exports.getInputPort = getInputPort;
|
|
|
11
11
|
exports.getStreamPort = getStreamPort;
|
|
12
12
|
exports.getInprocDylibPath = getInprocDylibPath;
|
|
13
13
|
exports.getHidBinaryPath = getHidBinaryPath;
|
|
14
|
+
exports.getFoldDylibPath = getFoldDylibPath;
|
|
14
15
|
exports.getCaptureBinaryPath = getCaptureBinaryPath;
|
|
15
16
|
exports.installDriver = installDriver;
|
|
16
17
|
exports.isSimulatorBooted = isSimulatorBooted;
|
|
@@ -363,6 +364,18 @@ async function getHidBinaryPath() {
|
|
|
363
364
|
const p = path_1.default.join(dir, 'ios-hid', 'conductor-hid');
|
|
364
365
|
return fs_1.default.existsSync(p) ? p : null;
|
|
365
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Absolute path to the fold controller dylib (`ios-fold/conductor-fold.dylib`),
|
|
369
|
+
* built by `packages/ios-fold/tools/build-fold.sh`. Injected into a foldable
|
|
370
|
+
* simulator's locationd to drive the hinge. Returns null if it hasn't been built.
|
|
371
|
+
*/
|
|
372
|
+
async function getFoldDylibPath() {
|
|
373
|
+
const dir = await getDriversDir().catch(() => null);
|
|
374
|
+
if (!dir)
|
|
375
|
+
return null;
|
|
376
|
+
const p = path_1.default.join(dir, 'ios-fold', 'conductor-fold.dylib');
|
|
377
|
+
return fs_1.default.existsSync(p) ? p : null;
|
|
378
|
+
}
|
|
366
379
|
/**
|
|
367
380
|
* Absolute path to the host-side Simulator video capture binary
|
|
368
381
|
* (`ios-capture/conductor-capture`), built by
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEVICECTL_ORIENTATIONS = void 0;
|
|
6
7
|
exports.parseDevicectlDevices = parseDevicectlDevices;
|
|
7
8
|
exports.listPhysicalDevices = listPhysicalDevices;
|
|
8
9
|
exports.findPhysicalDevice = findPhysicalDevice;
|
|
@@ -14,6 +15,9 @@ exports.uninstallApp = uninstallApp;
|
|
|
14
15
|
exports.launchApp = launchApp;
|
|
15
16
|
exports.terminateApp = terminateApp;
|
|
16
17
|
exports.listApps = listApps;
|
|
18
|
+
exports.getOrientation = getOrientation;
|
|
19
|
+
exports.setOrientation = setOrientation;
|
|
20
|
+
exports.listDisplays = listDisplays;
|
|
17
21
|
/**
|
|
18
22
|
* `xcrun devicectl` wrapper — the physical-device counterpart to `simctl`.
|
|
19
23
|
*
|
|
@@ -241,3 +245,51 @@ async function listApps(deviceId) {
|
|
|
241
245
|
name: a.name ?? a.bundleIdentifier,
|
|
242
246
|
}));
|
|
243
247
|
}
|
|
248
|
+
/** Orientations devicectl accepts; a superset of the driver's portrait/landscape. */
|
|
249
|
+
exports.DEVICECTL_ORIENTATIONS = [
|
|
250
|
+
'portrait',
|
|
251
|
+
'portraitUpsideDown',
|
|
252
|
+
'landscapeLeft',
|
|
253
|
+
'landscapeRight',
|
|
254
|
+
'faceUp',
|
|
255
|
+
'faceDown',
|
|
256
|
+
];
|
|
257
|
+
/**
|
|
258
|
+
* Read the device's current orientation. Works for simulators as well as
|
|
259
|
+
* physical devices — CoreDevice covers both — and needs no running test driver.
|
|
260
|
+
*/
|
|
261
|
+
async function getOrientation(deviceId) {
|
|
262
|
+
const out = await devicectl(['device', 'orientation', 'get', '--device', deviceId]);
|
|
263
|
+
// Devices that track flat poses report "Non-flat Orientation" instead.
|
|
264
|
+
const match = out.match(/Current Device (?:Non-flat )?Orientation:\s*(\w+)/);
|
|
265
|
+
if (!match)
|
|
266
|
+
throw new Error(`could not parse orientation from devicectl: ${out.trim()}`);
|
|
267
|
+
return match[1];
|
|
268
|
+
}
|
|
269
|
+
/** Set the device's orientation. */
|
|
270
|
+
async function setOrientation(deviceId, orientation) {
|
|
271
|
+
await devicectl(['device', 'orientation', 'set', '--device', deviceId, orientation]);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* List the device's displays. Foldables report two integrated panels and flag
|
|
275
|
+
* which one is live, which is the only reliable way to know where to point a
|
|
276
|
+
* screenshot: the swap follows the system's own transition logic, not the
|
|
277
|
+
* hinge angle.
|
|
278
|
+
*/
|
|
279
|
+
async function listDisplays(deviceId) {
|
|
280
|
+
const parsed = await devicectlJson(['device', 'info', 'displays', '--device', deviceId], 15000);
|
|
281
|
+
return (parsed.result?.displays ?? [])
|
|
282
|
+
.filter((d) => typeof d.displayId === 'number')
|
|
283
|
+
.map((d) => {
|
|
284
|
+
// `type` is a single-key object, e.g. { integrated: {} } or { carPlay: {} }.
|
|
285
|
+
const kind = Object.keys(d.type ?? {})[0] ?? '';
|
|
286
|
+
return {
|
|
287
|
+
displayId: d.displayId,
|
|
288
|
+
name: d.name ?? '',
|
|
289
|
+
active: d.active === true,
|
|
290
|
+
primary: d.primary === true,
|
|
291
|
+
kind,
|
|
292
|
+
integrated: kind === 'integrated',
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pickCaptureDisplay = pickCaptureDisplay;
|
|
4
|
+
/**
|
|
5
|
+
* Roles, which mean the same thing on any platform that grows a foldable.
|
|
6
|
+
* Everything else is addressed by display id: a device's own class and panel
|
|
7
|
+
* names (integrated/carPlay, LCD-1) are platform vocabulary that wouldn't carry
|
|
8
|
+
* over to Android, and every display it could name already has an id. Unknown
|
|
9
|
+
* values list the device's displays so the ids are discoverable.
|
|
10
|
+
*/
|
|
11
|
+
const ROLE_ALIASES = {
|
|
12
|
+
cover: 'primary',
|
|
13
|
+
main: 'primary',
|
|
14
|
+
primary: 'primary',
|
|
15
|
+
inner: 'secondary',
|
|
16
|
+
secondary: 'secondary',
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Pick the display to capture. With no override this is whichever panel is
|
|
20
|
+
* live, which keeps screenshots working across a fold without the caller
|
|
21
|
+
* having to think about it. Returns null for ordinary single-display devices
|
|
22
|
+
* so they keep using the driver path untouched.
|
|
23
|
+
*/
|
|
24
|
+
function pickCaptureDisplay(displays, override) {
|
|
25
|
+
const integrated = displays.filter((d) => d.integrated);
|
|
26
|
+
if (override) {
|
|
27
|
+
const key = override.trim().toLowerCase();
|
|
28
|
+
const id = Number(key);
|
|
29
|
+
if (Number.isInteger(id) && displays.some((d) => d.displayId === id)) {
|
|
30
|
+
return { displayId: id };
|
|
31
|
+
}
|
|
32
|
+
const role = ROLE_ALIASES[key];
|
|
33
|
+
if (role) {
|
|
34
|
+
const match = role === 'primary' ? integrated.find((d) => d.primary) : integrated.find((d) => !d.primary);
|
|
35
|
+
if (match)
|
|
36
|
+
return { displayId: match.displayId };
|
|
37
|
+
}
|
|
38
|
+
const known = displays
|
|
39
|
+
.map((d) => `${d.displayId} (${[d.name, d.kind].filter(Boolean).join(', ')})`)
|
|
40
|
+
.join('; ');
|
|
41
|
+
return {
|
|
42
|
+
displayId: null,
|
|
43
|
+
error: `this device has no '${override}' display — it reports: ${known}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Single-panel devices: nothing to choose, let the driver handle it.
|
|
47
|
+
if (integrated.length < 2)
|
|
48
|
+
return { displayId: null };
|
|
49
|
+
const live = integrated.find((d) => d.active) ?? integrated.find((d) => d.primary);
|
|
50
|
+
return { displayId: live ? live.displayId : null };
|
|
51
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RELAY_ORIENTATIONS = exports.FOLD_POSES = void 0;
|
|
7
|
+
exports.resolveFoldAngle = resolveFoldAngle;
|
|
8
|
+
exports.poseForAngle = poseForAngle;
|
|
9
|
+
exports.ensureFoldControllerInjected = ensureFoldControllerInjected;
|
|
10
|
+
exports.setFoldAngle = setFoldAngle;
|
|
11
|
+
exports.setOrientationViaRelay = setOrientationViaRelay;
|
|
12
|
+
exports.getFoldState = getFoldState;
|
|
13
|
+
/**
|
|
14
|
+
* Fold (hinge) control for foldable simulators, e.g. the iPhone Duo.
|
|
15
|
+
*
|
|
16
|
+
* Apple exposes no setter: `simctl` has nothing, and `devicectl` only *reads*
|
|
17
|
+
* the angle. The fold is driven by an AVP event that travels Device Hub ->
|
|
18
|
+
* CoreDevice remote HID -> the guest's dtuhidd -> locationd, whose CoreMotion
|
|
19
|
+
* relay synthesises the hinge HID event SpringBoard folds on. Conductor's own
|
|
20
|
+
* Indigo channel cannot carry it (backboardd rejects hinge events), so we
|
|
21
|
+
* inject `drivers/ios-fold/conductor-fold.dylib` into the simulator's locationd
|
|
22
|
+
* and feed that relay directly — see packages/ios-fold for the full rationale.
|
|
23
|
+
*
|
|
24
|
+
* Injection lasts until locationd restarts, so every call re-checks the pid the
|
|
25
|
+
* dylib recorded and re-injects when it no longer matches.
|
|
26
|
+
*/
|
|
27
|
+
const fs_1 = __importDefault(require("fs"));
|
|
28
|
+
const child_process_1 = require("child_process");
|
|
29
|
+
const util_1 = require("util");
|
|
30
|
+
const bootstrap_js_1 = require("./bootstrap.js");
|
|
31
|
+
const exec = (0, util_1.promisify)(child_process_1.execFile);
|
|
32
|
+
/** Named poses, mirroring Device Hub's three hinge buttons. */
|
|
33
|
+
exports.FOLD_POSES = {
|
|
34
|
+
closed: 0,
|
|
35
|
+
book: 130,
|
|
36
|
+
open: 180,
|
|
37
|
+
};
|
|
38
|
+
/** A simulator process's /tmp is the host's /tmp, so scope paths by device. */
|
|
39
|
+
const controlPath = (udid) => `/tmp/conductor-fold-${udid}`;
|
|
40
|
+
const orientationPath = (udid) => `/tmp/conductor-orientation-${udid}`;
|
|
41
|
+
/**
|
|
42
|
+
* The relay's own orientation vocabulary, which is not devicectl's spelling.
|
|
43
|
+
* Foldables ignore `devicectl device orientation set`, so they need this path.
|
|
44
|
+
*/
|
|
45
|
+
exports.RELAY_ORIENTATIONS = {
|
|
46
|
+
portrait: 'portrait',
|
|
47
|
+
portraitUpsideDown: 'pud',
|
|
48
|
+
landscapeLeft: 'landscape-left',
|
|
49
|
+
landscapeRight: 'landscape-right',
|
|
50
|
+
faceUp: 'faceup',
|
|
51
|
+
faceDown: 'facedown',
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a CLI value — a pose name or an angle in degrees — to an angle.
|
|
55
|
+
* Returns null for anything else, including an empty value: `Number('')` is 0,
|
|
56
|
+
* which would otherwise turn a bare `set-fold` into "fold the device shut".
|
|
57
|
+
*/
|
|
58
|
+
function resolveFoldAngle(value) {
|
|
59
|
+
const key = value.trim().toLowerCase();
|
|
60
|
+
if (!key)
|
|
61
|
+
return null;
|
|
62
|
+
if (key in exports.FOLD_POSES)
|
|
63
|
+
return exports.FOLD_POSES[key];
|
|
64
|
+
const angle = Number(key);
|
|
65
|
+
if (!Number.isFinite(angle) || angle < 0 || angle > 180)
|
|
66
|
+
return null;
|
|
67
|
+
return angle;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Name the pose when the angle is close to one, else call it partial. Which
|
|
71
|
+
* panel is lit is deliberately not derived from the angle: the system swaps
|
|
72
|
+
* displays on its own transition logic, so a hinge parked mid-way can still be
|
|
73
|
+
* driving the cover display.
|
|
74
|
+
*/
|
|
75
|
+
function poseForAngle(angle) {
|
|
76
|
+
for (const [name, value] of Object.entries(exports.FOLD_POSES)) {
|
|
77
|
+
if (Math.abs(value - angle) <= 10)
|
|
78
|
+
return name;
|
|
79
|
+
}
|
|
80
|
+
return 'partial';
|
|
81
|
+
}
|
|
82
|
+
async function locationdPid(udid) {
|
|
83
|
+
try {
|
|
84
|
+
const { stdout } = await exec('xcrun', [
|
|
85
|
+
'simctl',
|
|
86
|
+
'spawn',
|
|
87
|
+
udid,
|
|
88
|
+
'launchctl',
|
|
89
|
+
'print',
|
|
90
|
+
'system/com.apple.locationd',
|
|
91
|
+
]);
|
|
92
|
+
return stdout.match(/^\s*pid = (\d+)/m)?.[1] ?? null;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function recordedPid(udid) {
|
|
99
|
+
try {
|
|
100
|
+
return fs_1.default.readFileSync(`${controlPath(udid)}.pid`, 'utf8').trim() || null;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Ensure the fold controller is live inside locationd. DYLD_INSERT_LIBRARIES is
|
|
108
|
+
* set on the simulator's launchd only for the moment it takes to restart
|
|
109
|
+
* locationd — anything else launched in that window loads the dylib too, which
|
|
110
|
+
* is harmless (it no-ops outside locationd).
|
|
111
|
+
*/
|
|
112
|
+
async function ensureFoldControllerInjected(udid) {
|
|
113
|
+
const pid = await locationdPid(udid);
|
|
114
|
+
if (pid && pid === recordedPid(udid))
|
|
115
|
+
return;
|
|
116
|
+
// Restarting locationd resets CoreMotion's relay to a 0° hinge, which folds
|
|
117
|
+
// the device. Remember where the hinge was so it can be put back.
|
|
118
|
+
const previousAngle = await readHingeAngleViaDevicectl(udid).catch(() => null);
|
|
119
|
+
const dylib = await (0, bootstrap_js_1.getFoldDylibPath)();
|
|
120
|
+
if (!dylib) {
|
|
121
|
+
throw new Error('fold control requires drivers/ios-fold/conductor-fold.dylib — build it with packages/ios-fold/tools/build-fold.sh');
|
|
122
|
+
}
|
|
123
|
+
const spawn = (args) => exec('xcrun', ['simctl', 'spawn', udid, ...args]);
|
|
124
|
+
await spawn(['launchctl', 'setenv', 'DYLD_INSERT_LIBRARIES', dylib]);
|
|
125
|
+
try {
|
|
126
|
+
await spawn(['launchctl', 'kickstart', '-k', 'system/com.apple.locationd']);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await spawn(['launchctl', 'unsetenv', 'DYLD_INSERT_LIBRARIES']).catch(() => { });
|
|
130
|
+
}
|
|
131
|
+
const deadline = Date.now() + 15000;
|
|
132
|
+
while (Date.now() < deadline) {
|
|
133
|
+
const current = await locationdPid(udid);
|
|
134
|
+
if (current && current === recordedPid(udid)) {
|
|
135
|
+
if (previousAngle !== null) {
|
|
136
|
+
fs_1.default.writeFileSync(controlPath(udid), String(previousAngle));
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
141
|
+
}
|
|
142
|
+
throw new Error('fold controller did not come up inside locationd (is this a foldable device?)');
|
|
143
|
+
}
|
|
144
|
+
/** Apply a hinge angle in degrees (0 = closed, 180 = open flat). */
|
|
145
|
+
async function setFoldAngle(udid, angle) {
|
|
146
|
+
if (!Number.isFinite(angle) || angle < 0 || angle > 180) {
|
|
147
|
+
throw new Error(`fold angle must be between 0 and 180 (got ${angle})`);
|
|
148
|
+
}
|
|
149
|
+
await ensureFoldControllerInjected(udid);
|
|
150
|
+
fs_1.default.writeFileSync(controlPath(udid), String(angle));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Rotate through the injected relay, the way Device Hub's orientation picker
|
|
154
|
+
* does. Only needed where devicectl's setter is ignored.
|
|
155
|
+
*/
|
|
156
|
+
async function setOrientationViaRelay(udid, orientation) {
|
|
157
|
+
const token = exports.RELAY_ORIENTATIONS[orientation];
|
|
158
|
+
if (!token)
|
|
159
|
+
throw new Error(`no relay mapping for orientation '${orientation}'`);
|
|
160
|
+
await ensureFoldControllerInjected(udid);
|
|
161
|
+
fs_1.default.writeFileSync(orientationPath(udid), token);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read the current fold state. devicectl is the authority: the hinge can be
|
|
165
|
+
* moved from Device Hub as well as by us, so anything we cached locally would
|
|
166
|
+
* only cover the folds this process happened to see.
|
|
167
|
+
*/
|
|
168
|
+
async function getFoldState(udid) {
|
|
169
|
+
const angle = await readHingeAngleViaDevicectl(udid);
|
|
170
|
+
return { angle, pose: poseForAngle(angle) };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* `devicectl device motion hinge-angle` is a monitor: it prints the current
|
|
174
|
+
* angle immediately but then streams until its session expires, so take the
|
|
175
|
+
* first sample and stop it rather than waiting around.
|
|
176
|
+
*/
|
|
177
|
+
async function readHingeAngleViaDevicectl(udid) {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const proc = (0, child_process_1.spawn)('xcrun', [
|
|
180
|
+
'devicectl',
|
|
181
|
+
'device',
|
|
182
|
+
'motion',
|
|
183
|
+
'hinge-angle',
|
|
184
|
+
'--device',
|
|
185
|
+
udid,
|
|
186
|
+
'--session-timeout',
|
|
187
|
+
'10',
|
|
188
|
+
]);
|
|
189
|
+
let out = '';
|
|
190
|
+
let settled = false;
|
|
191
|
+
const finish = (fn) => {
|
|
192
|
+
if (settled)
|
|
193
|
+
return;
|
|
194
|
+
settled = true;
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
proc.kill('SIGTERM');
|
|
197
|
+
fn();
|
|
198
|
+
};
|
|
199
|
+
const timer = setTimeout(() => finish(() => reject(new Error('timed out reading hinge angle'))), 15000);
|
|
200
|
+
proc.stdout.on('data', (chunk) => {
|
|
201
|
+
out += chunk.toString();
|
|
202
|
+
const match = out.match(/Angle:\s*([0-9.]+)/);
|
|
203
|
+
if (match)
|
|
204
|
+
finish(() => resolve(Number(match[1])));
|
|
205
|
+
});
|
|
206
|
+
proc.on('error', (err) => finish(() => reject(err)));
|
|
207
|
+
proc.on('close', () => finish(() => reject(new Error('could not read hinge angle — is this a foldable device?'))));
|
|
208
|
+
});
|
|
209
|
+
}
|
package/dist/enum-options.js
CHANGED
|
@@ -48,8 +48,38 @@ exports.ENUM_PARAMS = [
|
|
|
48
48
|
command: 'set-orientation',
|
|
49
49
|
param: '<orientation>',
|
|
50
50
|
description: 'Device orientation',
|
|
51
|
-
// Source:
|
|
52
|
-
values: [
|
|
51
|
+
// Source: BASIC + IOS_ONLY in commands/set-orientation.ts
|
|
52
|
+
values: [
|
|
53
|
+
{ value: 'portrait' },
|
|
54
|
+
{ value: 'landscape' },
|
|
55
|
+
{ value: 'portraitUpsideDown', description: 'iOS only' },
|
|
56
|
+
{ value: 'landscapeLeft', description: 'iOS only' },
|
|
57
|
+
{ value: 'landscapeRight', description: 'iOS only' },
|
|
58
|
+
{ value: 'faceUp', description: 'iOS only' },
|
|
59
|
+
{ value: 'faceDown', description: 'iOS only' },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
command: 'take-screenshot',
|
|
64
|
+
param: '--display',
|
|
65
|
+
description: 'Which display to capture (default: whichever panel is live)',
|
|
66
|
+
// Source: ROLE_ALIASES in drivers/ios-displays.ts. Any display id the
|
|
67
|
+
// device reports is also accepted, so the list is not exhaustive.
|
|
68
|
+
values: [
|
|
69
|
+
{ value: 'cover', description: 'Foldable outer panel (XCUIScreen.main)' },
|
|
70
|
+
{ value: 'inner', description: 'Foldable inner panel, live when unfolded' },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
command: 'set-fold',
|
|
75
|
+
param: '<state>',
|
|
76
|
+
description: 'Hinge state of a foldable device, or an angle from 0 to 180',
|
|
77
|
+
// Source: FOLD_POSES in drivers/ios-fold.ts
|
|
78
|
+
values: [
|
|
79
|
+
{ value: 'closed', description: '0\u00b0 — folded shut, cover display active' },
|
|
80
|
+
{ value: 'book', description: '130\u00b0 — half open' },
|
|
81
|
+
{ value: 'open', description: '180\u00b0 — open flat' },
|
|
82
|
+
],
|
|
53
83
|
},
|
|
54
84
|
{
|
|
55
85
|
command: 'start-device',
|
package/dist/index.js
CHANGED
|
@@ -58,6 +58,8 @@ const hide_keyboard_js_1 = require("./commands/hide-keyboard.js");
|
|
|
58
58
|
const scroll_until_visible_js_1 = require("./commands/scroll-until-visible.js");
|
|
59
59
|
const set_location_js_1 = require("./commands/set-location.js");
|
|
60
60
|
const set_orientation_js_1 = require("./commands/set-orientation.js");
|
|
61
|
+
const fold_js_1 = require("./commands/fold.js");
|
|
62
|
+
const get_orientation_js_1 = require("./commands/get-orientation.js");
|
|
61
63
|
const set_viewport_js_1 = require("./commands/set-viewport.js");
|
|
62
64
|
const start_device_js_1 = require("./commands/start-device.js");
|
|
63
65
|
const stop_device_js_1 = require("./commands/stop-device.js");
|
|
@@ -140,6 +142,9 @@ const COMMAND_HELP = {
|
|
|
140
142
|
travel: travel_js_1.HELP,
|
|
141
143
|
'record-video': record_video_js_1.HELP,
|
|
142
144
|
'set-orientation': set_orientation_js_1.HELP,
|
|
145
|
+
'get-fold': fold_js_1.HELP,
|
|
146
|
+
'set-fold': fold_js_1.HELP,
|
|
147
|
+
'get-orientation': get_orientation_js_1.HELP,
|
|
143
148
|
'set-viewport': set_viewport_js_1.HELP,
|
|
144
149
|
'take-screenshot': screenshot_js_1.HELP,
|
|
145
150
|
'capture-ui': capture_ui_js_1.HELP,
|
|
@@ -823,6 +828,19 @@ async function main() {
|
|
|
823
828
|
});
|
|
824
829
|
break;
|
|
825
830
|
}
|
|
831
|
+
case 'get-fold': {
|
|
832
|
+
exitCode = await (0, fold_js_1.getFold)(opts, sessionName);
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
case 'set-fold': {
|
|
836
|
+
const value = String(rest[0] ?? argv['fold'] ?? '');
|
|
837
|
+
exitCode = await (0, fold_js_1.setFold)(value, opts, sessionName);
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
case 'get-orientation': {
|
|
841
|
+
exitCode = await (0, get_orientation_js_1.getOrientation)(opts, sessionName);
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
826
844
|
case 'set-orientation': {
|
|
827
845
|
const orientation = (rest[0] ??
|
|
828
846
|
argv['orientation'] ??
|
|
@@ -867,6 +885,7 @@ async function main() {
|
|
|
867
885
|
above: argv['above'],
|
|
868
886
|
leftOf: argv['left-of'],
|
|
869
887
|
rightOf: argv['right-of'],
|
|
888
|
+
display: argv['display'] !== undefined ? String(argv['display']) : undefined,
|
|
870
889
|
});
|
|
871
890
|
break;
|
|
872
891
|
}
|
package/package.json
CHANGED
|
@@ -27,10 +27,39 @@ conductor list-apps # installed app ids / package names (--json adds ap
|
|
|
27
27
|
| `conductor stop-device [<name-or-id>] [--all]` | Shut down device(s) |
|
|
28
28
|
| `conductor delete-device <name-or-id> [--all]` | Delete simulator(s)/AVD(s)/web session(s) |
|
|
29
29
|
| `conductor set-location --lat <n> --lng <n>` | Set GPS coordinates |
|
|
30
|
-
| `conductor set-orientation <portrait\|landscape>`
|
|
30
|
+
| `conductor set-orientation <portrait\|landscape\|portraitUpsideDown\|landscapeLeft\|landscapeRight\|faceUp\|faceDown>` | Set orientation (the last five are iOS only) |
|
|
31
|
+
| `conductor get-orientation` | Print the current orientation (iOS only) |
|
|
32
|
+
| `conductor set-fold <closed\|book\|open\|0-180>` | Fold/unfold a foldable device, or set an exact hinge angle (iPhone Duo) |
|
|
33
|
+
| `conductor get-fold` | Print the current hinge angle and pose |
|
|
31
34
|
| `conductor set-viewport [<w> <h>] [--preset mobile\|tablet\|desktop]` | Resize web viewport (web only) |
|
|
32
35
|
| `conductor install-web [--check] [browser]` | Install a Playwright browser (chromium/firefox/webkit); `--check` = status |
|
|
33
36
|
|
|
37
|
+
### Foldable devices (iPhone Duo)
|
|
38
|
+
|
|
39
|
+
`set-fold` drives the hinge the same way Device Hub's slider does, so the device
|
|
40
|
+
really folds: SpringBoard swaps between the cover and inner displays, and
|
|
41
|
+
`devicectl device motion hinge-angle` reports the new angle. `take-screenshot`
|
|
42
|
+
follows the fold automatically — it captures whichever panel is live, so an
|
|
43
|
+
unfolded device gives you the inner screen rather than the powered-off cover.
|
|
44
|
+
Pass `--display cover` or `--display inner` to pin it to one panel, or a display
|
|
45
|
+
id for anything else the device has attached (CarPlay, an external screen). An
|
|
46
|
+
unknown value lists that device's displays with their ids.
|
|
47
|
+
|
|
48
|
+
Angles are in degrees, 0 (shut) to 180 (flat). `closed`/`book`/`open` map to
|
|
49
|
+
0/130/180. Named poses always swap the display; an arbitrary mid-way angle sets
|
|
50
|
+
the hinge but may leave the cover display active, because the system decides
|
|
51
|
+
when to swap on its own transition logic.
|
|
52
|
+
|
|
53
|
+
The first `set-fold` after a device boots takes a few seconds: it injects a
|
|
54
|
+
controller into the simulator's locationd (restarting it), because Apple ships
|
|
55
|
+
no fold setter in `simctl` or `devicectl`. The current angle is restored
|
|
56
|
+
afterwards, so injecting doesn't change the pose. Subsequent calls are instant. This is
|
|
57
|
+
iOS-simulator only.
|
|
58
|
+
|
|
59
|
+
`set-orientation` works on foldables too, but by the same route: they accept
|
|
60
|
+
`devicectl`'s setter and ignore it, so the command verifies the result and
|
|
61
|
+
rotates through the injected controller when needed.
|
|
62
|
+
|
|
34
63
|
### Attach to an existing browser (CDP)
|
|
35
64
|
|
|
36
65
|
Instead of launching its own browser, the web driver can attach to one that's
|
|
@@ -17,7 +17,7 @@ here. Always observe before you act, and confirm after.
|
|
|
17
17
|
| `conductor inspect [--dump]` | Print the UI hierarchy (`--dump` = raw driver output) |
|
|
18
18
|
| `conductor inspect --at <x,y> [--tappable]` | Topmost view at a screen point |
|
|
19
19
|
| `conductor focused [--poll [ms]]` | Metadata of the focused element. `--poll` watches changes — only with a bounded use, then stop it |
|
|
20
|
-
| `conductor take-screenshot [<element>] [--output <path>] [--full-page]` | Screenshot; crop to a matched element; `--full-page` (web) |
|
|
20
|
+
| `conductor take-screenshot [<element>] [--output <path>] [--full-page] [--display <panel>]` | Screenshot; crop to a matched element; `--full-page` (web); `--display cover\|inner\|<id>` picks a display (default: whichever panel is live) |
|
|
21
21
|
|
|
22
22
|
`capture-ui` is the workhorse: it returns the screen as structured data **and**
|
|
23
23
|
gives each element a ref like `@e3` that `conductor tap-on @e3` taps by cached
|