@houwert/conductor 0.33.2 → 0.33.3
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.
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.HELP = void 0;
|
|
7
7
|
exports.captureScreen = captureScreen;
|
|
8
|
+
exports.iosHierarchySize = iosHierarchySize;
|
|
9
|
+
exports.cropRect = cropRect;
|
|
8
10
|
exports.screenshot = screenshot;
|
|
9
11
|
exports.HELP = ` take-screenshot [<element>] [--output <path>] [--full-page] [--display <panel>]
|
|
10
12
|
Take screenshot (--full-page: web only, capture entire scrollable page)
|
|
@@ -75,7 +77,8 @@ async function captureScreen(driver, opts, displayOverride) {
|
|
|
75
77
|
const primary = displays.find((d) => d.primary);
|
|
76
78
|
// Nothing to redirect: the driver already captures the primary panel.
|
|
77
79
|
if (choice.displayId === null || (primary && choice.displayId === primary.displayId)) {
|
|
78
|
-
|
|
80
|
+
const live = choice.displayId === null || primary?.active !== false;
|
|
81
|
+
return { buffer: await driver.screenshot(opts), redirected: false, live };
|
|
79
82
|
}
|
|
80
83
|
const file = path_1.default.join(os_1.default.tmpdir(), `conductor-shot-${Date.now()}.png`);
|
|
81
84
|
try {
|
|
@@ -88,7 +91,13 @@ async function captureScreen(driver, opts, displayOverride) {
|
|
|
88
91
|
String(choice.displayId),
|
|
89
92
|
file,
|
|
90
93
|
]);
|
|
91
|
-
|
|
94
|
+
const panel = displays.find((d) => d.displayId === choice.displayId);
|
|
95
|
+
return {
|
|
96
|
+
buffer: await promises_1.default.readFile(file),
|
|
97
|
+
redirected: true,
|
|
98
|
+
scale: panel?.pointScale,
|
|
99
|
+
live: panel?.active,
|
|
100
|
+
};
|
|
92
101
|
}
|
|
93
102
|
catch (err) {
|
|
94
103
|
throw new Error(`could not capture display ${choice.displayId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -97,6 +106,38 @@ async function captureScreen(driver, opts, displayOverride) {
|
|
|
97
106
|
await promises_1.default.unlink(file).catch(() => { });
|
|
98
107
|
}
|
|
99
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* The size of the coordinate space the view hierarchy reports, in its own
|
|
111
|
+
* units, for an iOS capture.
|
|
112
|
+
*
|
|
113
|
+
* `deviceInfo()` describes `XCUIScreen.main`, which is the wrong panel for a
|
|
114
|
+
* redirected capture: on an unfolded foldable the hierarchy is in the inner
|
|
115
|
+
* panel's points while deviceInfo still reports the cover's. Fall back to the
|
|
116
|
+
* captured image divided by that panel's own scale, which is exact.
|
|
117
|
+
*/
|
|
118
|
+
function iosHierarchySize(capture, png, deviceInfo) {
|
|
119
|
+
if (capture.redirected && capture.scale) {
|
|
120
|
+
return { width: png.width / capture.scale, height: png.height / capture.scale };
|
|
121
|
+
}
|
|
122
|
+
return { width: deviceInfo.widthPoints, height: deviceInfo.heightPoints };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Map element bounds onto screenshot pixels. The margin is in the same logical
|
|
126
|
+
* units as the bounds (points on iOS, pixels on Android/Web — what `inspect`
|
|
127
|
+
* prints), so it scales alongside them.
|
|
128
|
+
*/
|
|
129
|
+
function cropRect(bounds, hierarchy, png, margin) {
|
|
130
|
+
const scaleX = hierarchy.width > 0 ? png.width / hierarchy.width : 1;
|
|
131
|
+
const scaleY = hierarchy.height > 0 ? png.height / hierarchy.height : 1;
|
|
132
|
+
const marginX = margin * scaleX;
|
|
133
|
+
const marginY = margin * scaleY;
|
|
134
|
+
return {
|
|
135
|
+
x: Math.round(bounds.x * scaleX - marginX),
|
|
136
|
+
y: Math.round(bounds.y * scaleY - marginY),
|
|
137
|
+
width: Math.round(bounds.width * scaleX + marginX * 2),
|
|
138
|
+
height: Math.round(bounds.height * scaleY + marginY * 2),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
100
141
|
async function screenshot(outputPath, opts = {}, sessionName = 'default', fullPage = false, query = '', flags = {}) {
|
|
101
142
|
const timestamp = Date.now();
|
|
102
143
|
const defaultName = `screenshot-${timestamp}.png`;
|
|
@@ -127,12 +168,12 @@ async function screenshot(outputPath, opts = {}, sessionName = 'default', fullPa
|
|
|
127
168
|
: '';
|
|
128
169
|
const margin = flags.margin ?? DEFAULT_MARGIN_PX;
|
|
129
170
|
const result = await (0, runner_js_1.runDirect)(async (driver) => {
|
|
130
|
-
const { buffer: buf, redirected } = await captureScreen(driver, { fullPage }, flags.display);
|
|
171
|
+
const { buffer: buf, redirected, scale, live, } = await captureScreen(driver, { fullPage }, flags.display);
|
|
131
172
|
let out = buf;
|
|
132
|
-
if (sel &&
|
|
133
|
-
throw new Error(
|
|
134
|
-
'
|
|
135
|
-
'
|
|
173
|
+
if (sel && live === false) {
|
|
174
|
+
throw new Error(`cannot crop to ${label} on this display: it is powered off, and the view ` +
|
|
175
|
+
'hierarchy describes whichever panel is live. Drop --display to capture ' +
|
|
176
|
+
'the live panel, or fold the device so this one takes over.');
|
|
136
177
|
}
|
|
137
178
|
if (sel) {
|
|
138
179
|
let el;
|
|
@@ -143,10 +184,12 @@ async function screenshot(outputPath, opts = {}, sessionName = 'default', fullPa
|
|
|
143
184
|
// around the foreground app + status bars and has frame=.zero, so
|
|
144
185
|
// reading scale from it would always collapse to 1× and crop the
|
|
145
186
|
// wrong region on retina/4K screens. Use deviceInfo, which reports
|
|
146
|
-
// both points (AX space) and pixels (screenshot space)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
187
|
+
// both points (AX space) and pixels (screenshot space) — except for a
|
|
188
|
+
// redirected capture, where deviceInfo describes XCUIScreen.main and
|
|
189
|
+
// not the panel we captured, so derive the points from its own scale.
|
|
190
|
+
const size = iosHierarchySize({ redirected, scale }, (0, png_crop_js_1.readPngDimensions)(buf), await driver.deviceInfo());
|
|
191
|
+
hierarchyW = size.width;
|
|
192
|
+
hierarchyH = size.height;
|
|
150
193
|
el = await (0, wait_js_1.waitForIOSElement)((o) => driver.viewHierarchy(false, [], { cache: o?.cached }).then((x) => x.axElement), sel, undefined, undefined, (0, direct_ios_selector_js_1.makeIOSDirectResolver)(driver, sel));
|
|
151
194
|
}
|
|
152
195
|
else if (driver instanceof web_js_1.WebDriver) {
|
|
@@ -170,21 +213,14 @@ async function screenshot(outputPath, opts = {}, sessionName = 'default', fullPa
|
|
|
170
213
|
throw new Error('selector cropping is not supported for this driver');
|
|
171
214
|
}
|
|
172
215
|
const { width: pngW, height: pngH } = (0, png_crop_js_1.readPngDimensions)(buf);
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const marginY = margin * scaleY;
|
|
180
|
-
const rectX = Math.round(el.bounds.x * scaleX - marginX);
|
|
181
|
-
const rectY = Math.round(el.bounds.y * scaleY - marginY);
|
|
182
|
-
const rectW = Math.round(el.bounds.width * scaleX + marginX * 2);
|
|
183
|
-
const rectH = Math.round(el.bounds.height * scaleY + marginY * 2);
|
|
184
|
-
if (rectX + rectW <= 0 || rectY + rectH <= 0 || rectX >= pngW || rectY >= pngH) {
|
|
185
|
-
throw new Error(`element ${label} bounds [${rectX},${rectY} ${rectW}x${rectH}] are outside the screenshot (${pngW}x${pngH})`);
|
|
216
|
+
const rect = cropRect(el.bounds, { width: hierarchyW, height: hierarchyH }, { width: pngW, height: pngH }, margin);
|
|
217
|
+
if (rect.x + rect.width <= 0 ||
|
|
218
|
+
rect.y + rect.height <= 0 ||
|
|
219
|
+
rect.x >= pngW ||
|
|
220
|
+
rect.y >= pngH) {
|
|
221
|
+
throw new Error(`element ${label} bounds [${rect.x},${rect.y} ${rect.width}x${rect.height}] are outside the screenshot (${pngW}x${pngH})`);
|
|
186
222
|
}
|
|
187
|
-
out = (0, png_crop_js_1.cropPng)(buf,
|
|
223
|
+
out = (0, png_crop_js_1.cropPng)(buf, rect);
|
|
188
224
|
}
|
|
189
225
|
await promises_1.default.writeFile(resolvedPath, out);
|
|
190
226
|
}, sessionName);
|
package/package.json
CHANGED
|
@@ -45,10 +45,10 @@ Pass `--display cover` or `--display inner` to pin it to one panel, or a display
|
|
|
45
45
|
id for anything else the device has attached (CarPlay, an external screen). An
|
|
46
46
|
unknown value lists that device's displays with their ids.
|
|
47
47
|
|
|
48
|
-
Cropping
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
Cropping to an element (`take-screenshot <element>`) works on either panel. The
|
|
49
|
+
one thing it cannot do is crop against a panel that is powered off — the view
|
|
50
|
+
hierarchy always describes the live one — so combining a selector with a
|
|
51
|
+
`--display` that points at the dark panel is an error rather than a wrong crop.
|
|
52
52
|
|
|
53
53
|
Angles are in degrees, 0 (shut) to 180 (flat). `closed`/`book`/`open` map to
|
|
54
54
|
0/130/180. Named poses always swap the display; an arbitrary mid-way angle sets
|