codeplay-common 4.3.5 → 4.3.7
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/.gitattributes +2 -2
- package/LICENSE +21 -21
- package/README.md +125 -125
- package/files/buildCodeplay/codeplayBeforeBuild-6.2.js +4107 -4107
- package/files/buildCodeplay/versions.json +23 -23
- package/files/finalrelease +7 -7
- package/files/generate-ai-promo-video.js +1525 -0
- package/files/iap-install-2.js +145 -145
- package/files/ionic.config.json +6 -6
- package/files/scripts/setup-baseline-profile.js +262 -262
- package/files/take-screen-image.js +1418 -1284
- package/files/take-screen-video.js +25 -32
- package/package.json +15 -15
- package/scripts/sync-files.js +103 -103
- package/scripts/uninstall.js +109 -109
|
@@ -1,76 +1,81 @@
|
|
|
1
|
-
const fs = require('node:fs');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const readline = require('node:readline/promises');
|
|
4
|
-
const { spawn, spawnSync } = require('node:child_process');
|
|
5
|
-
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const readline = require('node:readline/promises');
|
|
4
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
5
|
+
|
|
6
6
|
const screenshotDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-image-Screenshot');
|
|
7
7
|
const inFrameDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-image-Frame');
|
|
8
8
|
const amazonAssetsDirectory = path.join(__dirname, 'Auto-Screenshot', 'Amazon-assets');
|
|
9
9
|
const samsungAssetsDirectory = path.join(__dirname, 'Auto-Screenshot', 'Samsung-assets');
|
|
10
10
|
const capacitorConfigPath = path.join(__dirname, 'capacitor.config.json');
|
|
11
11
|
const amazonIconPath = path.join(__dirname, 'resources', 'icon-only.png');
|
|
12
|
-
const temporaryDirectory = path.join(__dirname, 'agent-temp');
|
|
13
|
-
const browserExecutablePaths = [
|
|
14
|
-
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
15
|
-
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
16
|
-
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
17
|
-
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
const deviceProfiles = [
|
|
21
|
-
{
|
|
22
|
-
name: 'android-phone',
|
|
23
|
-
label: 'Android phone',
|
|
24
|
-
outputDirectory: 'Android',
|
|
25
|
-
frameType: 'android-phone',
|
|
26
|
-
width: 360,
|
|
12
|
+
const temporaryDirectory = path.join(__dirname, 'agent-temp');
|
|
13
|
+
const browserExecutablePaths = [
|
|
14
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
15
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
16
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
17
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const deviceProfiles = [
|
|
21
|
+
{
|
|
22
|
+
name: 'android-phone',
|
|
23
|
+
label: 'Android phone',
|
|
24
|
+
outputDirectory: 'Android',
|
|
25
|
+
frameType: 'android-phone',
|
|
26
|
+
width: 360,
|
|
27
27
|
height: 800,
|
|
28
28
|
deviceScaleFactor: 3,
|
|
29
29
|
orientationSensitive: true,
|
|
30
30
|
mobile: true,
|
|
31
|
-
},
|
|
31
|
+
},
|
|
32
32
|
{
|
|
33
|
-
name: 'android-tablet',
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
name: 'android-tablet-7-inch',
|
|
34
|
+
aliases: ['android-tablet'],
|
|
35
|
+
label: 'Android 7-inch Play Store tablet',
|
|
36
|
+
outputDirectory: 'Android-Tab-7inch',
|
|
37
|
+
frameType: 'tablet',
|
|
38
|
+
width: 675,
|
|
39
|
+
height: 1200,
|
|
39
40
|
deviceScaleFactor: 2,
|
|
41
|
+
framedWidth: 1350,
|
|
42
|
+
framedHeight: 2400,
|
|
40
43
|
orientationSensitive: true,
|
|
41
44
|
mobile: true,
|
|
42
45
|
},
|
|
43
46
|
{
|
|
44
47
|
name: 'android-tablet-10-inch',
|
|
45
48
|
label: 'Android 10-inch Play Store tablet',
|
|
46
|
-
outputDirectory: 'Android-Tab-
|
|
49
|
+
outputDirectory: 'Android-Tab-10inch',
|
|
47
50
|
frameType: 'tablet',
|
|
48
|
-
width:
|
|
49
|
-
height:
|
|
51
|
+
width: 900,
|
|
52
|
+
height: 1600,
|
|
50
53
|
deviceScaleFactor: 2,
|
|
54
|
+
framedWidth: 1800,
|
|
55
|
+
framedHeight: 3200,
|
|
51
56
|
orientationSensitive: true,
|
|
52
57
|
mobile: true,
|
|
53
58
|
},
|
|
54
59
|
{
|
|
55
60
|
name: 'iphone',
|
|
56
|
-
label: 'iPhone 6.9-inch App Store viewport',
|
|
57
|
-
outputDirectory: 'iPhone',
|
|
58
|
-
frameType: 'iphone',
|
|
59
|
-
width: 440,
|
|
60
|
-
height: 956,
|
|
61
|
+
label: 'iPhone 6.9-inch App Store viewport',
|
|
62
|
+
outputDirectory: 'iPhone',
|
|
63
|
+
frameType: 'iphone',
|
|
64
|
+
width: 440,
|
|
65
|
+
height: 956,
|
|
61
66
|
deviceScaleFactor: 3,
|
|
62
67
|
framedWidth: 1284,
|
|
63
68
|
framedHeight: 2778,
|
|
64
69
|
orientationSensitive: true,
|
|
65
70
|
mobile: true,
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: 'ipad',
|
|
69
|
-
label: 'iPad 13-inch App Store viewport',
|
|
70
|
-
outputDirectory: 'iPad',
|
|
71
|
-
frameType: 'tablet',
|
|
72
|
-
width: 1032,
|
|
73
|
-
height: 1376,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'ipad',
|
|
74
|
+
label: 'iPad 13-inch App Store viewport',
|
|
75
|
+
outputDirectory: 'iPad',
|
|
76
|
+
frameType: 'tablet',
|
|
77
|
+
width: 1032,
|
|
78
|
+
height: 1376,
|
|
74
79
|
deviceScaleFactor: 2,
|
|
75
80
|
framedWidth: 2048,
|
|
76
81
|
framedHeight: 2732,
|
|
@@ -113,78 +118,80 @@ const deviceProfiles = [
|
|
|
113
118
|
framedHeight: 514,
|
|
114
119
|
mobile: true,
|
|
115
120
|
},
|
|
116
|
-
{
|
|
117
|
-
name: 'amazon-mobile',
|
|
118
|
-
label: 'Amazon mobile store viewport',
|
|
119
|
-
outputDirectory: 'Amazon-Phone',
|
|
120
|
-
frameType: 'android-phone',
|
|
121
|
-
width: 360,
|
|
121
|
+
{
|
|
122
|
+
name: 'amazon-mobile',
|
|
123
|
+
label: 'Amazon mobile store viewport',
|
|
124
|
+
outputDirectory: 'Amazon-Phone',
|
|
125
|
+
frameType: 'android-phone',
|
|
126
|
+
width: 360,
|
|
122
127
|
height: 640,
|
|
123
128
|
deviceScaleFactor: 3,
|
|
124
129
|
orientationSensitive: true,
|
|
125
130
|
mobile: true,
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
name: 'amazon-fire-tablet',
|
|
129
|
-
label: 'Amazon Fire tablet store viewport',
|
|
130
|
-
outputDirectory: 'Amazon-Tab',
|
|
131
|
-
frameType: 'tablet',
|
|
132
|
-
width: 800,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'amazon-fire-tablet',
|
|
134
|
+
label: 'Amazon Fire tablet store viewport',
|
|
135
|
+
outputDirectory: 'Amazon-Tab',
|
|
136
|
+
frameType: 'tablet',
|
|
137
|
+
width: 800,
|
|
133
138
|
height: 1280,
|
|
134
139
|
deviceScaleFactor: 2,
|
|
135
140
|
orientationSensitive: true,
|
|
136
141
|
mobile: true,
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
name: 'amazon-fire-tv',
|
|
140
|
-
label: 'Amazon Fire TV store viewport',
|
|
141
|
-
outputDirectory: 'Amazon-TV',
|
|
142
|
-
frameType: 'tv',
|
|
143
|
-
width: 1920,
|
|
144
|
-
height: 1080,
|
|
145
|
-
deviceScaleFactor: 1,
|
|
146
|
-
mobile: false,
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
name: 'android-tv',
|
|
150
|
-
label: 'Android TV',
|
|
151
|
-
outputDirectory: 'Android-TV',
|
|
152
|
-
frameType: 'tv',
|
|
153
|
-
width: 1920,
|
|
154
|
-
height: 1080,
|
|
155
|
-
deviceScaleFactor: 1,
|
|
156
|
-
mobile: false,
|
|
157
|
-
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'amazon-fire-tv',
|
|
145
|
+
label: 'Amazon Fire TV store viewport',
|
|
146
|
+
outputDirectory: 'Amazon-TV',
|
|
147
|
+
frameType: 'tv',
|
|
148
|
+
width: 1920,
|
|
149
|
+
height: 1080,
|
|
150
|
+
deviceScaleFactor: 1,
|
|
151
|
+
mobile: false,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'android-tv',
|
|
155
|
+
label: 'Android TV',
|
|
156
|
+
outputDirectory: 'Android-TV',
|
|
157
|
+
frameType: 'tv',
|
|
158
|
+
width: 1920,
|
|
159
|
+
height: 1080,
|
|
160
|
+
deviceScaleFactor: 1,
|
|
161
|
+
mobile: false,
|
|
162
|
+
},
|
|
158
163
|
{
|
|
159
164
|
name: 'chromebook',
|
|
160
|
-
label: 'Chromebook Play Store viewport',
|
|
165
|
+
label: 'Chromebook 4K Play Store viewport',
|
|
161
166
|
outputDirectory: 'Chromebook',
|
|
162
167
|
frameType: 'laptop',
|
|
163
168
|
width: 1920,
|
|
164
169
|
height: 1080,
|
|
170
|
+
deviceScaleFactor: 2,
|
|
171
|
+
framedWidth: 3840,
|
|
172
|
+
framedHeight: 2160,
|
|
173
|
+
mobile: false,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'android-xr',
|
|
177
|
+
label: 'Android XR 2D panel approximation',
|
|
178
|
+
outputDirectory: 'Android-XR',
|
|
179
|
+
frameType: 'xr',
|
|
180
|
+
width: 1920,
|
|
181
|
+
height: 1080,
|
|
165
182
|
deviceScaleFactor: 1,
|
|
166
183
|
mobile: false,
|
|
167
184
|
},
|
|
168
|
-
{
|
|
169
|
-
name: '
|
|
170
|
-
label: '
|
|
171
|
-
outputDirectory: '
|
|
172
|
-
frameType: '
|
|
173
|
-
width:
|
|
174
|
-
height:
|
|
175
|
-
deviceScaleFactor:
|
|
176
|
-
mobile: false,
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
name: 'mac',
|
|
180
|
-
label: 'Mac-sized viewport',
|
|
181
|
-
outputDirectory: 'Mac',
|
|
182
|
-
frameType: 'laptop',
|
|
183
|
-
width: 1440,
|
|
184
|
-
height: 900,
|
|
185
|
-
deviceScaleFactor: 2,
|
|
186
|
-
mobile: false,
|
|
187
|
-
},
|
|
185
|
+
{
|
|
186
|
+
name: 'mac',
|
|
187
|
+
label: 'Mac-sized viewport',
|
|
188
|
+
outputDirectory: 'Mac',
|
|
189
|
+
frameType: 'laptop',
|
|
190
|
+
width: 1440,
|
|
191
|
+
height: 900,
|
|
192
|
+
deviceScaleFactor: 2,
|
|
193
|
+
mobile: false,
|
|
194
|
+
},
|
|
188
195
|
];
|
|
189
196
|
|
|
190
197
|
function getConfiguredOrientation() {
|
|
@@ -207,192 +214,192 @@ function orientProfile(profile, orientation) {
|
|
|
207
214
|
framedHeight: profile.framedWidth,
|
|
208
215
|
};
|
|
209
216
|
}
|
|
210
|
-
|
|
211
|
-
function runAdb(args, options = {}) {
|
|
212
|
-
const result = spawnSync('adb', args, {
|
|
213
|
-
encoding: 'utf8',
|
|
214
|
-
timeout: 30000,
|
|
215
|
-
windowsHide: true,
|
|
216
|
-
...options,
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
if (result.error) {
|
|
220
|
-
if (result.error.code === 'ENOENT') {
|
|
221
|
-
throw new Error('ADB was not found. Add Android platform-tools to your PATH.');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (result.error.code === 'ETIMEDOUT') {
|
|
225
|
-
throw new Error('ADB did not respond within 30 seconds. Check the device connection.');
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
throw result.error;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (result.status !== 0) {
|
|
232
|
-
throw new Error(result.stderr.trim() || `ADB exited with code ${result.status}.`);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return result.stdout.trim();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function getConnectedDevice() {
|
|
239
|
-
const devices = runAdb(['devices'])
|
|
240
|
-
.split(/\r?\n/)
|
|
241
|
-
.map((line) => line.match(/^(\S+)\s+device$/))
|
|
242
|
-
.filter(Boolean)
|
|
243
|
-
.map((match) => match[1]);
|
|
244
|
-
|
|
245
|
-
if (!devices.length) {
|
|
246
|
-
throw new Error('no_connected_device');
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return devices[0];
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function getAppId() {
|
|
253
|
-
const config = JSON.parse(fs.readFileSync(capacitorConfigPath, 'utf8'));
|
|
254
|
-
|
|
255
|
-
if (!config.appId) {
|
|
256
|
-
throw new Error('capacitor.config.json does not contain an appId.');
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return config.appId;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function getWebViewSocket(deviceId, appId) {
|
|
263
|
-
const processIds = runAdb(['-s', deviceId, 'shell', 'pidof', appId])
|
|
264
|
-
.split(/\s+/)
|
|
265
|
-
.filter(Boolean);
|
|
266
|
-
|
|
267
|
-
if (!processIds.length) {
|
|
268
|
-
throw new Error(`The app process is not running: ${appId}`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const sockets = runAdb(['-s', deviceId, 'shell', 'cat', '/proc/net/unix']);
|
|
272
|
-
|
|
273
|
-
for (const processId of processIds) {
|
|
274
|
-
const socketName = `webview_devtools_remote_${processId}`;
|
|
275
|
-
|
|
276
|
-
if (sockets.includes(`@${socketName}`)) {
|
|
277
|
-
return socketName;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
throw new Error(
|
|
282
|
-
'The running app does not expose a debuggable WebView. Use a debug/live-reload build with WebView debugging enabled.',
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function sanitizeName(input, fallback) {
|
|
287
|
-
const sanitized = input
|
|
288
|
-
.trim()
|
|
289
|
-
.replace(/[<>:"/\\|?*\u0000-\u001F]/g, '_')
|
|
290
|
-
.replace(/[. ]+$/g, '');
|
|
291
|
-
|
|
292
|
-
return sanitized || fallback;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function getSelectedOutputDirectories(selectedProfiles) {
|
|
296
|
-
return [...new Set(selectedProfiles.flatMap((profile) => ([
|
|
297
|
-
path.join(screenshotDirectory, profile.outputDirectory),
|
|
298
|
-
path.join(inFrameDirectory, profile.outputDirectory),
|
|
299
|
-
])))];
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function getNextNumberedFileName(selectedProfiles) {
|
|
303
|
-
const highestNumber = getSelectedOutputDirectories(selectedProfiles)
|
|
304
|
-
.filter((directory) => fs.existsSync(directory))
|
|
305
|
-
.reduce((highestAcrossDirectories, directory) => {
|
|
306
|
-
const highestInDirectory = fs.readdirSync(directory, { withFileTypes: true })
|
|
307
|
-
.filter((entry) => entry.isFile())
|
|
308
|
-
.reduce((highest, entry) => {
|
|
309
|
-
const match = entry.name.match(/^(\d+)\.png$/i);
|
|
310
|
-
return match ? Math.max(highest, Number(match[1])) : highest;
|
|
311
|
-
}, 0);
|
|
312
|
-
|
|
313
|
-
return Math.max(highestAcrossDirectories, highestInDirectory);
|
|
314
|
-
}, 0);
|
|
315
|
-
|
|
316
|
-
return `${highestNumber + 1}.png`;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function getUniqueFileName(prefix, selectedProfiles) {
|
|
320
|
-
const outputDirectories = getSelectedOutputDirectories(selectedProfiles);
|
|
321
|
-
let fileName = `${prefix}.png`;
|
|
322
|
-
let duplicateNumber = 2;
|
|
323
|
-
|
|
324
|
-
while (outputDirectories.some((directory) => (
|
|
325
|
-
fs.existsSync(path.join(directory, fileName))
|
|
326
|
-
))) {
|
|
327
|
-
fileName = `${prefix}-${duplicateNumber}.png`;
|
|
328
|
-
duplicateNumber += 1;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return fileName;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function getScreenshotFileName(fileNameInput, selectedProfiles) {
|
|
335
|
-
const inputWithoutExtension = fileNameInput.trim().replace(/\.png$/i, '');
|
|
336
|
-
|
|
337
|
-
if (!inputWithoutExtension) {
|
|
338
|
-
return getNextNumberedFileName(selectedProfiles);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
let prefix = sanitizeName(inputWithoutExtension, '');
|
|
342
|
-
|
|
343
|
-
if (!prefix) {
|
|
344
|
-
return getNextNumberedFileName(selectedProfiles);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(prefix)) {
|
|
348
|
-
prefix = `${prefix}_`;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return getUniqueFileName(prefix, selectedProfiles);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function parseArguments() {
|
|
355
|
-
const args = process.argv.slice(2);
|
|
217
|
+
|
|
218
|
+
function runAdb(args, options = {}) {
|
|
219
|
+
const result = spawnSync('adb', args, {
|
|
220
|
+
encoding: 'utf8',
|
|
221
|
+
timeout: 30000,
|
|
222
|
+
windowsHide: true,
|
|
223
|
+
...options,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (result.error) {
|
|
227
|
+
if (result.error.code === 'ENOENT') {
|
|
228
|
+
throw new Error('ADB was not found. Add Android platform-tools to your PATH.');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (result.error.code === 'ETIMEDOUT') {
|
|
232
|
+
throw new Error('ADB did not respond within 30 seconds. Check the device connection.');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
throw result.error;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (result.status !== 0) {
|
|
239
|
+
throw new Error(result.stderr.trim() || `ADB exited with code ${result.status}.`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return result.stdout.trim();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getConnectedDevice() {
|
|
246
|
+
const devices = runAdb(['devices'])
|
|
247
|
+
.split(/\r?\n/)
|
|
248
|
+
.map((line) => line.match(/^(\S+)\s+device$/))
|
|
249
|
+
.filter(Boolean)
|
|
250
|
+
.map((match) => match[1]);
|
|
251
|
+
|
|
252
|
+
if (!devices.length) {
|
|
253
|
+
throw new Error('no_connected_device');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return devices[0];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function getAppId() {
|
|
260
|
+
const config = JSON.parse(fs.readFileSync(capacitorConfigPath, 'utf8'));
|
|
261
|
+
|
|
262
|
+
if (!config.appId) {
|
|
263
|
+
throw new Error('capacitor.config.json does not contain an appId.');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return config.appId;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getWebViewSocket(deviceId, appId) {
|
|
270
|
+
const processIds = runAdb(['-s', deviceId, 'shell', 'pidof', appId])
|
|
271
|
+
.split(/\s+/)
|
|
272
|
+
.filter(Boolean);
|
|
273
|
+
|
|
274
|
+
if (!processIds.length) {
|
|
275
|
+
throw new Error(`The app process is not running: ${appId}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const sockets = runAdb(['-s', deviceId, 'shell', 'cat', '/proc/net/unix']);
|
|
279
|
+
|
|
280
|
+
for (const processId of processIds) {
|
|
281
|
+
const socketName = `webview_devtools_remote_${processId}`;
|
|
282
|
+
|
|
283
|
+
if (sockets.includes(`@${socketName}`)) {
|
|
284
|
+
return socketName;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
throw new Error(
|
|
289
|
+
'The running app does not expose a debuggable WebView. Use a debug/live-reload build with WebView debugging enabled.',
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function sanitizeName(input, fallback) {
|
|
294
|
+
const sanitized = input
|
|
295
|
+
.trim()
|
|
296
|
+
.replace(/[<>:"/\\|?*\u0000-\u001F]/g, '_')
|
|
297
|
+
.replace(/[. ]+$/g, '');
|
|
298
|
+
|
|
299
|
+
return sanitized || fallback;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getSelectedOutputDirectories(selectedProfiles) {
|
|
303
|
+
return [...new Set(selectedProfiles.flatMap((profile) => ([
|
|
304
|
+
path.join(screenshotDirectory, profile.outputDirectory),
|
|
305
|
+
path.join(inFrameDirectory, profile.outputDirectory),
|
|
306
|
+
])))];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getNextNumberedFileName(selectedProfiles) {
|
|
310
|
+
const highestNumber = getSelectedOutputDirectories(selectedProfiles)
|
|
311
|
+
.filter((directory) => fs.existsSync(directory))
|
|
312
|
+
.reduce((highestAcrossDirectories, directory) => {
|
|
313
|
+
const highestInDirectory = fs.readdirSync(directory, { withFileTypes: true })
|
|
314
|
+
.filter((entry) => entry.isFile())
|
|
315
|
+
.reduce((highest, entry) => {
|
|
316
|
+
const match = entry.name.match(/^(\d+)\.png$/i);
|
|
317
|
+
return match ? Math.max(highest, Number(match[1])) : highest;
|
|
318
|
+
}, 0);
|
|
319
|
+
|
|
320
|
+
return Math.max(highestAcrossDirectories, highestInDirectory);
|
|
321
|
+
}, 0);
|
|
322
|
+
|
|
323
|
+
return `${highestNumber + 1}.png`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function getUniqueFileName(prefix, selectedProfiles) {
|
|
327
|
+
const outputDirectories = getSelectedOutputDirectories(selectedProfiles);
|
|
328
|
+
let fileName = `${prefix}.png`;
|
|
329
|
+
let duplicateNumber = 2;
|
|
330
|
+
|
|
331
|
+
while (outputDirectories.some((directory) => (
|
|
332
|
+
fs.existsSync(path.join(directory, fileName))
|
|
333
|
+
))) {
|
|
334
|
+
fileName = `${prefix}-${duplicateNumber}.png`;
|
|
335
|
+
duplicateNumber += 1;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return fileName;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function getScreenshotFileName(fileNameInput, selectedProfiles) {
|
|
342
|
+
const inputWithoutExtension = fileNameInput.trim().replace(/\.png$/i, '');
|
|
343
|
+
|
|
344
|
+
if (!inputWithoutExtension) {
|
|
345
|
+
return getNextNumberedFileName(selectedProfiles);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
let prefix = sanitizeName(inputWithoutExtension, '');
|
|
349
|
+
|
|
350
|
+
if (!prefix) {
|
|
351
|
+
return getNextNumberedFileName(selectedProfiles);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(prefix)) {
|
|
355
|
+
prefix = `${prefix}_`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return getUniqueFileName(prefix, selectedProfiles);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function parseArguments() {
|
|
362
|
+
const args = process.argv.slice(2);
|
|
356
363
|
const options = {
|
|
357
364
|
amazonAssets: false,
|
|
358
365
|
samsungAssets: false,
|
|
359
|
-
dryRun: false,
|
|
360
|
-
frameExisting: false,
|
|
361
|
-
help: false,
|
|
362
|
-
list: false,
|
|
363
|
-
prefix: '',
|
|
364
|
-
profiles: '',
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
368
|
-
const argument = args[index];
|
|
369
|
-
|
|
366
|
+
dryRun: false,
|
|
367
|
+
frameExisting: false,
|
|
368
|
+
help: false,
|
|
369
|
+
list: false,
|
|
370
|
+
prefix: '',
|
|
371
|
+
profiles: '',
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
375
|
+
const argument = args[index];
|
|
376
|
+
|
|
370
377
|
if (argument === '--amazon-assets') {
|
|
371
378
|
options.amazonAssets = true;
|
|
372
379
|
} else if (argument === '--samsung-assets') {
|
|
373
380
|
options.samsungAssets = true;
|
|
374
381
|
} else if (argument === '--dry-run') {
|
|
375
|
-
options.dryRun = true;
|
|
376
|
-
} else if (argument === '--frame-existing') {
|
|
377
|
-
options.frameExisting = true;
|
|
378
|
-
} else if (argument === '--help' || argument === '-h') {
|
|
379
|
-
options.help = true;
|
|
380
|
-
} else if (argument === '--list') {
|
|
381
|
-
options.list = true;
|
|
382
|
-
} else if (argument === '--prefix') {
|
|
383
|
-
options.prefix = args[index + 1] || '';
|
|
384
|
-
index += 1;
|
|
385
|
-
} else if (argument === '--profiles') {
|
|
386
|
-
options.profiles = args[index + 1] || '';
|
|
387
|
-
index += 1;
|
|
388
|
-
} else {
|
|
389
|
-
throw new Error(`Unknown argument: ${argument}`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return options;
|
|
394
|
-
}
|
|
395
|
-
|
|
382
|
+
options.dryRun = true;
|
|
383
|
+
} else if (argument === '--frame-existing') {
|
|
384
|
+
options.frameExisting = true;
|
|
385
|
+
} else if (argument === '--help' || argument === '-h') {
|
|
386
|
+
options.help = true;
|
|
387
|
+
} else if (argument === '--list') {
|
|
388
|
+
options.list = true;
|
|
389
|
+
} else if (argument === '--prefix') {
|
|
390
|
+
options.prefix = args[index + 1] || '';
|
|
391
|
+
index += 1;
|
|
392
|
+
} else if (argument === '--profiles') {
|
|
393
|
+
options.profiles = args[index + 1] || '';
|
|
394
|
+
index += 1;
|
|
395
|
+
} else {
|
|
396
|
+
throw new Error(`Unknown argument: ${argument}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return options;
|
|
401
|
+
}
|
|
402
|
+
|
|
396
403
|
function printProfiles() {
|
|
397
404
|
const orientation = getConfiguredOrientation();
|
|
398
405
|
|
|
@@ -410,11 +417,11 @@ function printProfiles() {
|
|
|
410
417
|
);
|
|
411
418
|
}
|
|
412
419
|
}
|
|
413
|
-
|
|
414
|
-
function printHelp() {
|
|
415
|
-
console.log('Capture the current live Android WebView at multiple responsive viewport sizes.');
|
|
416
|
-
console.log('');
|
|
417
|
-
console.log('Usage:');
|
|
420
|
+
|
|
421
|
+
function printHelp() {
|
|
422
|
+
console.log('Capture the current live Android WebView at multiple responsive viewport sizes.');
|
|
423
|
+
console.log('');
|
|
424
|
+
console.log('Usage:');
|
|
418
425
|
console.log(' node take-screen-image.js');
|
|
419
426
|
console.log(' node take-screen-image.js --profiles iphone,ipad');
|
|
420
427
|
console.log(' node take-screen-image.js --prefix home --profiles android-phone,ipad');
|
|
@@ -423,462 +430,464 @@ function printHelp() {
|
|
|
423
430
|
console.log(' node take-screen-image.js --samsung-assets');
|
|
424
431
|
console.log(' node take-screen-image.js --dry-run');
|
|
425
432
|
console.log(' node take-screen-image.js --list');
|
|
426
|
-
}
|
|
427
|
-
|
|
433
|
+
}
|
|
434
|
+
|
|
428
435
|
function selectProfiles(input) {
|
|
429
436
|
const orientation = getConfiguredOrientation();
|
|
430
437
|
const names = input
|
|
431
|
-
.split(',')
|
|
432
|
-
.map((name) => name.trim().toLowerCase())
|
|
433
|
-
.filter(Boolean);
|
|
434
|
-
|
|
438
|
+
.split(',')
|
|
439
|
+
.map((name) => name.trim().toLowerCase())
|
|
440
|
+
.filter(Boolean);
|
|
441
|
+
|
|
435
442
|
if (!names.length || names.includes('all')) {
|
|
436
443
|
return deviceProfiles.map((profile) => orientProfile(profile, orientation));
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const selectedProfiles = names.map((name) => {
|
|
440
|
-
const profile = deviceProfiles.find((candidate) =>
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const selectedProfiles = names.map((name) => {
|
|
447
|
+
const profile = deviceProfiles.find((candidate) => (
|
|
448
|
+
candidate.name === name || candidate.aliases?.includes(name)
|
|
449
|
+
));
|
|
450
|
+
|
|
451
|
+
if (!profile) {
|
|
452
|
+
throw new Error(`Unknown profile: ${name}. Run with --list to see valid profiles.`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return profile;
|
|
456
|
+
});
|
|
457
|
+
|
|
449
458
|
return [...new Map(selectedProfiles.map((profile) => [profile.name, profile])).values()]
|
|
450
459
|
.map((profile) => orientProfile(profile, orientation));
|
|
451
460
|
}
|
|
452
|
-
|
|
453
|
-
class CdpClient {
|
|
454
|
-
constructor(webSocketUrl) {
|
|
455
|
-
this.nextId = 1;
|
|
456
|
-
this.pendingCommands = new Map();
|
|
457
|
-
this.socket = new WebSocket(webSocketUrl);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
connect() {
|
|
461
|
-
return new Promise((resolve, reject) => {
|
|
462
|
-
const timeout = setTimeout(() => {
|
|
463
|
-
reject(new Error('Timed out while connecting to the app WebView.'));
|
|
464
|
-
}, 10000);
|
|
465
|
-
|
|
466
|
-
this.socket.addEventListener('open', () => {
|
|
467
|
-
clearTimeout(timeout);
|
|
468
|
-
resolve();
|
|
469
|
-
}, { once: true });
|
|
470
|
-
|
|
471
|
-
this.socket.addEventListener('error', () => {
|
|
472
|
-
clearTimeout(timeout);
|
|
473
|
-
reject(new Error('Could not connect to the app WebView debugging socket.'));
|
|
474
|
-
}, { once: true });
|
|
475
|
-
|
|
476
|
-
this.socket.addEventListener('message', (event) => {
|
|
477
|
-
const message = JSON.parse(event.data);
|
|
478
|
-
|
|
479
|
-
if (!message.id || !this.pendingCommands.has(message.id)) {
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const pendingCommand = this.pendingCommands.get(message.id);
|
|
484
|
-
this.pendingCommands.delete(message.id);
|
|
485
|
-
|
|
486
|
-
if (message.error) {
|
|
487
|
-
pendingCommand.reject(new Error(message.error.message));
|
|
488
|
-
} else {
|
|
489
|
-
pendingCommand.resolve(message.result);
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
this.socket.addEventListener('close', () => {
|
|
494
|
-
for (const pendingCommand of this.pendingCommands.values()) {
|
|
495
|
-
pendingCommand.reject(new Error('The app WebView debugging connection closed.'));
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
this.pendingCommands.clear();
|
|
499
|
-
});
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
send(method, params = {}) {
|
|
504
|
-
return new Promise((resolve, reject) => {
|
|
505
|
-
const id = this.nextId;
|
|
506
|
-
this.nextId += 1;
|
|
507
|
-
this.pendingCommands.set(id, { resolve, reject });
|
|
508
|
-
this.socket.send(JSON.stringify({ id, method, params }));
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
close() {
|
|
513
|
-
this.socket.close();
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
async function getWebViewTarget(localPort) {
|
|
518
|
-
const response = await fetch(`http://127.0.0.1:${localPort}/json/list`);
|
|
519
|
-
|
|
520
|
-
if (!response.ok) {
|
|
521
|
-
throw new Error(`WebView target discovery failed with HTTP ${response.status}.`);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const targets = await response.json();
|
|
525
|
-
const pageTargets = targets.filter((target) => (
|
|
526
|
-
target.type === 'page'
|
|
527
|
-
&& target.webSocketDebuggerUrl
|
|
528
|
-
&& target.url !== 'about:blank'
|
|
529
|
-
));
|
|
530
|
-
|
|
531
|
-
const visibleTarget = pageTargets.find((target) => {
|
|
532
|
-
try {
|
|
533
|
-
return JSON.parse(target.description || '{}').visible !== false;
|
|
534
|
-
} catch {
|
|
535
|
-
return true;
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
const target = visibleTarget || pageTargets[0];
|
|
540
|
-
|
|
541
|
-
if (!target) {
|
|
542
|
-
throw new Error('No rendered page was found in the app WebView.');
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
return target;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
async function getPageDetails(client) {
|
|
549
|
-
const evaluation = await client.send('Runtime.evaluate', {
|
|
550
|
-
expression: `JSON.stringify({
|
|
551
|
-
title: document.title,
|
|
552
|
-
url: location.href,
|
|
553
|
-
width: innerWidth,
|
|
554
|
-
height: innerHeight,
|
|
555
|
-
devicePixelRatio: devicePixelRatio,
|
|
556
|
-
readyState: document.readyState
|
|
557
|
-
})`,
|
|
558
|
-
returnByValue: true,
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
return JSON.parse(evaluation.result.value);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
async function capturePageSnapshot(client) {
|
|
565
|
-
const evaluation = await client.send('Runtime.evaluate', {
|
|
566
|
-
expression: `(async () => {
|
|
567
|
-
const clonedRoot = document.documentElement.cloneNode(true);
|
|
568
|
-
const originalElements = [document.documentElement, ...document.querySelectorAll('*')];
|
|
569
|
-
const clonedElements = [clonedRoot, ...clonedRoot.querySelectorAll('*')];
|
|
570
|
-
|
|
571
|
-
const blobToDataUrl = (blob) => new Promise((resolve, reject) => {
|
|
572
|
-
const reader = new FileReader();
|
|
573
|
-
reader.onload = () => resolve(reader.result);
|
|
574
|
-
reader.onerror = () => reject(reader.error);
|
|
575
|
-
reader.readAsDataURL(blob);
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
const imageToDataUrl = (element) => {
|
|
579
|
-
const width = element.naturalWidth || element.clientWidth;
|
|
580
|
-
const height = element.naturalHeight || element.clientHeight;
|
|
581
|
-
|
|
582
|
-
if (!width || !height) return '';
|
|
583
|
-
|
|
584
|
-
const canvas = document.createElement('canvas');
|
|
585
|
-
canvas.width = width;
|
|
586
|
-
canvas.height = height;
|
|
587
|
-
canvas.getContext('2d').drawImage(element, 0, 0, width, height);
|
|
588
|
-
return canvas.toDataURL('image/png');
|
|
589
|
-
};
|
|
590
|
-
|
|
591
|
-
for (let index = 0; index < originalElements.length; index += 1) {
|
|
592
|
-
const original = originalElements[index];
|
|
593
|
-
const clone = clonedElements[index];
|
|
594
|
-
|
|
595
|
-
if (!clone) continue;
|
|
596
|
-
|
|
597
|
-
if (original.scrollTop || original.scrollLeft) {
|
|
598
|
-
clone.setAttribute('data-responsive-scroll-top', String(original.scrollTop));
|
|
599
|
-
clone.setAttribute('data-responsive-scroll-left', String(original.scrollLeft));
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (original instanceof HTMLInputElement) {
|
|
603
|
-
clone.setAttribute('value', original.value);
|
|
604
|
-
clone.toggleAttribute('checked', original.checked);
|
|
605
|
-
} else if (original instanceof HTMLTextAreaElement) {
|
|
606
|
-
clone.textContent = original.value;
|
|
607
|
-
} else if (original instanceof HTMLSelectElement) {
|
|
608
|
-
for (let optionIndex = 0; optionIndex < original.options.length; optionIndex += 1) {
|
|
609
|
-
clone.options[optionIndex].selected = original.options[optionIndex].selected;
|
|
610
|
-
}
|
|
611
|
-
} else if (original instanceof HTMLCanvasElement) {
|
|
612
|
-
try {
|
|
613
|
-
const replacement = document.createElement('img');
|
|
614
|
-
replacement.src = original.toDataURL('image/png');
|
|
615
|
-
replacement.alt = original.getAttribute('aria-label') || '';
|
|
616
|
-
replacement.setAttribute('style', original.getAttribute('style') || '');
|
|
617
|
-
clone.replaceWith(replacement);
|
|
618
|
-
} catch {
|
|
619
|
-
// Keep the canvas element when its pixels cannot be exported.
|
|
620
|
-
}
|
|
621
|
-
} else if (original instanceof HTMLVideoElement && original.readyState >= 2) {
|
|
622
|
-
try {
|
|
623
|
-
const replacement = document.createElement('img');
|
|
624
|
-
replacement.src = imageToDataUrl(original);
|
|
625
|
-
replacement.alt = original.getAttribute('aria-label') || '';
|
|
626
|
-
replacement.setAttribute('style', original.getAttribute('style') || '');
|
|
627
|
-
clone.replaceWith(replacement);
|
|
628
|
-
} catch {
|
|
629
|
-
clone.setAttribute('poster', original.poster || '');
|
|
630
|
-
}
|
|
631
|
-
} else if (original instanceof HTMLImageElement && original.complete) {
|
|
632
|
-
try {
|
|
633
|
-
clone.setAttribute('src', imageToDataUrl(original) || original.currentSrc || original.src);
|
|
634
|
-
clone.removeAttribute('srcset');
|
|
635
|
-
} catch {
|
|
636
|
-
clone.setAttribute('src', original.currentSrc || original.src);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
const stylesheetLinks = [...document.querySelectorAll('link[rel="stylesheet"]')];
|
|
642
|
-
const clonedStylesheetLinks = [...clonedRoot.querySelectorAll('link[rel="stylesheet"]')];
|
|
643
|
-
|
|
644
|
-
await Promise.all(stylesheetLinks.map(async (link, index) => {
|
|
645
|
-
try {
|
|
646
|
-
const response = await fetch(link.href);
|
|
647
|
-
if (!response.ok) return;
|
|
648
|
-
|
|
649
|
-
const cssText = (await response.text()).replace(
|
|
650
|
-
/url\\(\\s*(['"]?)(?!data:|blob:|#)([^'"\\)]+)\\1\\s*\\)/gi,
|
|
651
|
-
(match, quote, resourceUrl) => {
|
|
652
|
-
try {
|
|
653
|
-
return 'url("' + new URL(resourceUrl.trim(), link.href).href + '")';
|
|
654
|
-
} catch {
|
|
655
|
-
return match;
|
|
656
|
-
}
|
|
657
|
-
},
|
|
658
|
-
);
|
|
659
|
-
const style = document.createElement('style');
|
|
660
|
-
style.textContent = cssText;
|
|
661
|
-
clonedStylesheetLinks[index].replaceWith(style);
|
|
662
|
-
} catch {
|
|
663
|
-
clonedStylesheetLinks[index].href = link.href;
|
|
664
|
-
}
|
|
665
|
-
}));
|
|
666
|
-
|
|
667
|
-
clonedRoot.querySelectorAll('script, link[rel="modulepreload"]').forEach((element) => element.remove());
|
|
668
|
-
clonedRoot.querySelectorAll('meta[http-equiv="Content-Security-Policy"]').forEach(
|
|
669
|
-
(element) => element.remove(),
|
|
670
|
-
);
|
|
671
|
-
|
|
672
|
-
const base = document.createElement('base');
|
|
673
|
-
base.href = location.href;
|
|
674
|
-
clonedRoot.querySelector('head').prepend(base);
|
|
675
|
-
|
|
676
|
-
const replayStyle = document.createElement('style');
|
|
677
|
-
replayStyle.textContent = [
|
|
678
|
-
'*,*::before,*::after{animation-play-state:paused!important;}',
|
|
679
|
-
'*{scrollbar-width:none!important;}',
|
|
680
|
-
'*::-webkit-scrollbar{width:0!important;height:0!important;display:none!important;}',
|
|
681
|
-
].join('');
|
|
682
|
-
clonedRoot.querySelector('head').append(replayStyle);
|
|
683
|
-
|
|
684
|
-
return {
|
|
685
|
-
html: '<!doctype html>' + clonedRoot.outerHTML,
|
|
686
|
-
scrollX,
|
|
687
|
-
scrollY,
|
|
688
|
-
};
|
|
689
|
-
})()`,
|
|
690
|
-
awaitPromise: true,
|
|
691
|
-
returnByValue: true,
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
if (evaluation.exceptionDetails) {
|
|
695
|
-
throw new Error('The current WebView page could not be serialized.');
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
return evaluation.result.value;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function findBrowserExecutable() {
|
|
702
|
-
const executablePath = browserExecutablePaths.find((candidate) => (
|
|
703
|
-
fs.existsSync(candidate)
|
|
704
|
-
));
|
|
705
|
-
|
|
706
|
-
if (!executablePath) {
|
|
707
|
-
throw new Error('Google Chrome or Microsoft Edge was not found on this computer.');
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
return executablePath;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function delay(milliseconds) {
|
|
714
|
-
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
async function launchReplayBrowser() {
|
|
718
|
-
fs.mkdirSync(temporaryDirectory, { recursive: true });
|
|
719
|
-
|
|
720
|
-
const profileDirectory = path.join(
|
|
721
|
-
temporaryDirectory,
|
|
722
|
-
`responsive-screenshot-chrome-${process.pid}-${Date.now()}`,
|
|
723
|
-
);
|
|
724
|
-
fs.mkdirSync(profileDirectory, { recursive: true });
|
|
725
|
-
|
|
726
|
-
const browserProcess = spawn(findBrowserExecutable(), [
|
|
727
|
-
'--headless=new',
|
|
728
|
-
'--remote-debugging-port=0',
|
|
729
|
-
`--user-data-dir=${profileDirectory}`,
|
|
730
|
-
'--no-first-run',
|
|
731
|
-
'--no-default-browser-check',
|
|
732
|
-
'--disable-background-networking',
|
|
733
|
-
'--remote-allow-origins=*',
|
|
734
|
-
'about:blank',
|
|
735
|
-
], {
|
|
736
|
-
stdio: 'ignore',
|
|
737
|
-
windowsHide: true,
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
const activePortPath = path.join(profileDirectory, 'DevToolsActivePort');
|
|
741
|
-
const deadline = Date.now() + 15000;
|
|
742
|
-
|
|
743
|
-
while (!fs.existsSync(activePortPath)) {
|
|
744
|
-
if (browserProcess.exitCode !== null) {
|
|
745
|
-
throw new Error('The local replay browser closed before it was ready.');
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
if (Date.now() >= deadline) {
|
|
749
|
-
browserProcess.kill();
|
|
750
|
-
throw new Error('Timed out while starting the local replay browser.');
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
await delay(100);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const [port] = fs.readFileSync(activePortPath, 'utf8').trim().split(/\r?\n/);
|
|
757
|
-
const versionResponse = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
758
|
-
const version = await versionResponse.json();
|
|
759
|
-
const client = new CdpClient(version.webSocketDebuggerUrl);
|
|
760
|
-
await client.connect();
|
|
761
|
-
|
|
762
|
-
return {
|
|
763
|
-
browserProcess,
|
|
764
|
-
client,
|
|
765
|
-
port,
|
|
766
|
-
profileDirectory,
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
async function createReplayPage(browser) {
|
|
771
|
-
const createdTarget = await browser.client.send('Target.createTarget', {
|
|
772
|
-
url: 'about:blank',
|
|
773
|
-
});
|
|
774
|
-
const deadline = Date.now() + 10000;
|
|
775
|
-
|
|
776
|
-
while (Date.now() < deadline) {
|
|
777
|
-
const response = await fetch(`http://127.0.0.1:${browser.port}/json/list`);
|
|
778
|
-
const targets = await response.json();
|
|
779
|
-
const target = targets.find((candidate) => candidate.id === createdTarget.targetId);
|
|
780
|
-
|
|
781
|
-
if (target && target.webSocketDebuggerUrl) {
|
|
782
|
-
const client = new CdpClient(target.webSocketDebuggerUrl);
|
|
783
|
-
await client.connect();
|
|
784
|
-
await client.send('Page.enable');
|
|
785
|
-
await client.send('Runtime.enable');
|
|
786
|
-
return client;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
await delay(100);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
throw new Error('The local replay page did not become available.');
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function removeTemporaryBrowserProfile(profileDirectory) {
|
|
796
|
-
const resolvedTemporaryRoot = path.resolve(temporaryDirectory);
|
|
797
|
-
const resolvedProfileDirectory = path.resolve(profileDirectory);
|
|
798
|
-
|
|
799
|
-
if (!resolvedProfileDirectory.startsWith(`${resolvedTemporaryRoot}${path.sep}`)) {
|
|
800
|
-
throw new Error('Refusing to remove a browser profile outside agent-temp.');
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
fs.rmSync(resolvedProfileDirectory, { recursive: true, force: true });
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
async function closeReplayBrowser(browser) {
|
|
807
|
-
try {
|
|
808
|
-
await browser.client.send('Browser.close');
|
|
809
|
-
} catch {
|
|
810
|
-
// Closing the browser can close the protocol connection before it acknowledges.
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
const deadline = Date.now() + 3000;
|
|
814
|
-
|
|
815
|
-
while (browser.browserProcess.exitCode === null && Date.now() < deadline) {
|
|
816
|
-
await delay(100);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
if (browser.browserProcess.exitCode === null) {
|
|
820
|
-
browser.browserProcess.kill();
|
|
821
|
-
await delay(200);
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
browser.client.close();
|
|
825
|
-
|
|
826
|
-
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
827
|
-
try {
|
|
828
|
-
removeTemporaryBrowserProfile(browser.profileDirectory);
|
|
829
|
-
return;
|
|
830
|
-
} catch (error) {
|
|
831
|
-
if (attempt === 5) {
|
|
832
|
-
throw error;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
await delay(200);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
async function waitForReplayLayout(client, snapshot) {
|
|
841
|
-
await client.send('Runtime.evaluate', {
|
|
842
|
-
expression: `(async () => {
|
|
843
|
-
const assetWait = Promise.all([...document.images].map((image) => {
|
|
844
|
-
if (image.complete) return Promise.resolve();
|
|
845
|
-
return new Promise((resolve) => {
|
|
846
|
-
image.addEventListener('load', resolve, { once: true });
|
|
847
|
-
image.addEventListener('error', resolve, { once: true });
|
|
848
|
-
});
|
|
849
|
-
}));
|
|
850
|
-
|
|
851
|
-
await Promise.race([
|
|
852
|
-
Promise.all([document.fonts.ready, assetWait]),
|
|
853
|
-
new Promise((resolve) => setTimeout(resolve, 5000)),
|
|
854
|
-
]);
|
|
855
|
-
|
|
856
|
-
document.querySelectorAll('[data-responsive-scroll-top]').forEach((element) => {
|
|
857
|
-
element.scrollTop = Number(element.dataset.responsiveScrollTop);
|
|
858
|
-
element.scrollLeft = Number(element.dataset.responsiveScrollLeft);
|
|
859
|
-
});
|
|
860
|
-
scrollTo(${Number(snapshot.scrollX) || 0}, ${Number(snapshot.scrollY) || 0});
|
|
861
|
-
|
|
862
|
-
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
863
|
-
})()`,
|
|
864
|
-
awaitPromise: true,
|
|
865
|
-
returnByValue: true,
|
|
866
|
-
});
|
|
867
|
-
}
|
|
868
|
-
|
|
461
|
+
|
|
462
|
+
class CdpClient {
|
|
463
|
+
constructor(webSocketUrl) {
|
|
464
|
+
this.nextId = 1;
|
|
465
|
+
this.pendingCommands = new Map();
|
|
466
|
+
this.socket = new WebSocket(webSocketUrl);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
connect() {
|
|
470
|
+
return new Promise((resolve, reject) => {
|
|
471
|
+
const timeout = setTimeout(() => {
|
|
472
|
+
reject(new Error('Timed out while connecting to the app WebView.'));
|
|
473
|
+
}, 10000);
|
|
474
|
+
|
|
475
|
+
this.socket.addEventListener('open', () => {
|
|
476
|
+
clearTimeout(timeout);
|
|
477
|
+
resolve();
|
|
478
|
+
}, { once: true });
|
|
479
|
+
|
|
480
|
+
this.socket.addEventListener('error', () => {
|
|
481
|
+
clearTimeout(timeout);
|
|
482
|
+
reject(new Error('Could not connect to the app WebView debugging socket.'));
|
|
483
|
+
}, { once: true });
|
|
484
|
+
|
|
485
|
+
this.socket.addEventListener('message', (event) => {
|
|
486
|
+
const message = JSON.parse(event.data);
|
|
487
|
+
|
|
488
|
+
if (!message.id || !this.pendingCommands.has(message.id)) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const pendingCommand = this.pendingCommands.get(message.id);
|
|
493
|
+
this.pendingCommands.delete(message.id);
|
|
494
|
+
|
|
495
|
+
if (message.error) {
|
|
496
|
+
pendingCommand.reject(new Error(message.error.message));
|
|
497
|
+
} else {
|
|
498
|
+
pendingCommand.resolve(message.result);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
this.socket.addEventListener('close', () => {
|
|
503
|
+
for (const pendingCommand of this.pendingCommands.values()) {
|
|
504
|
+
pendingCommand.reject(new Error('The app WebView debugging connection closed.'));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
this.pendingCommands.clear();
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
send(method, params = {}) {
|
|
513
|
+
return new Promise((resolve, reject) => {
|
|
514
|
+
const id = this.nextId;
|
|
515
|
+
this.nextId += 1;
|
|
516
|
+
this.pendingCommands.set(id, { resolve, reject });
|
|
517
|
+
this.socket.send(JSON.stringify({ id, method, params }));
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
close() {
|
|
522
|
+
this.socket.close();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function getWebViewTarget(localPort) {
|
|
527
|
+
const response = await fetch(`http://127.0.0.1:${localPort}/json/list`);
|
|
528
|
+
|
|
529
|
+
if (!response.ok) {
|
|
530
|
+
throw new Error(`WebView target discovery failed with HTTP ${response.status}.`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const targets = await response.json();
|
|
534
|
+
const pageTargets = targets.filter((target) => (
|
|
535
|
+
target.type === 'page'
|
|
536
|
+
&& target.webSocketDebuggerUrl
|
|
537
|
+
&& target.url !== 'about:blank'
|
|
538
|
+
));
|
|
539
|
+
|
|
540
|
+
const visibleTarget = pageTargets.find((target) => {
|
|
541
|
+
try {
|
|
542
|
+
return JSON.parse(target.description || '{}').visible !== false;
|
|
543
|
+
} catch {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const target = visibleTarget || pageTargets[0];
|
|
549
|
+
|
|
550
|
+
if (!target) {
|
|
551
|
+
throw new Error('No rendered page was found in the app WebView.');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return target;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function getPageDetails(client) {
|
|
558
|
+
const evaluation = await client.send('Runtime.evaluate', {
|
|
559
|
+
expression: `JSON.stringify({
|
|
560
|
+
title: document.title,
|
|
561
|
+
url: location.href,
|
|
562
|
+
width: innerWidth,
|
|
563
|
+
height: innerHeight,
|
|
564
|
+
devicePixelRatio: devicePixelRatio,
|
|
565
|
+
readyState: document.readyState
|
|
566
|
+
})`,
|
|
567
|
+
returnByValue: true,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
return JSON.parse(evaluation.result.value);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function capturePageSnapshot(client) {
|
|
574
|
+
const evaluation = await client.send('Runtime.evaluate', {
|
|
575
|
+
expression: `(async () => {
|
|
576
|
+
const clonedRoot = document.documentElement.cloneNode(true);
|
|
577
|
+
const originalElements = [document.documentElement, ...document.querySelectorAll('*')];
|
|
578
|
+
const clonedElements = [clonedRoot, ...clonedRoot.querySelectorAll('*')];
|
|
579
|
+
|
|
580
|
+
const blobToDataUrl = (blob) => new Promise((resolve, reject) => {
|
|
581
|
+
const reader = new FileReader();
|
|
582
|
+
reader.onload = () => resolve(reader.result);
|
|
583
|
+
reader.onerror = () => reject(reader.error);
|
|
584
|
+
reader.readAsDataURL(blob);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const imageToDataUrl = (element) => {
|
|
588
|
+
const width = element.naturalWidth || element.clientWidth;
|
|
589
|
+
const height = element.naturalHeight || element.clientHeight;
|
|
590
|
+
|
|
591
|
+
if (!width || !height) return '';
|
|
592
|
+
|
|
593
|
+
const canvas = document.createElement('canvas');
|
|
594
|
+
canvas.width = width;
|
|
595
|
+
canvas.height = height;
|
|
596
|
+
canvas.getContext('2d').drawImage(element, 0, 0, width, height);
|
|
597
|
+
return canvas.toDataURL('image/png');
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
for (let index = 0; index < originalElements.length; index += 1) {
|
|
601
|
+
const original = originalElements[index];
|
|
602
|
+
const clone = clonedElements[index];
|
|
603
|
+
|
|
604
|
+
if (!clone) continue;
|
|
605
|
+
|
|
606
|
+
if (original.scrollTop || original.scrollLeft) {
|
|
607
|
+
clone.setAttribute('data-responsive-scroll-top', String(original.scrollTop));
|
|
608
|
+
clone.setAttribute('data-responsive-scroll-left', String(original.scrollLeft));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (original instanceof HTMLInputElement) {
|
|
612
|
+
clone.setAttribute('value', original.value);
|
|
613
|
+
clone.toggleAttribute('checked', original.checked);
|
|
614
|
+
} else if (original instanceof HTMLTextAreaElement) {
|
|
615
|
+
clone.textContent = original.value;
|
|
616
|
+
} else if (original instanceof HTMLSelectElement) {
|
|
617
|
+
for (let optionIndex = 0; optionIndex < original.options.length; optionIndex += 1) {
|
|
618
|
+
clone.options[optionIndex].selected = original.options[optionIndex].selected;
|
|
619
|
+
}
|
|
620
|
+
} else if (original instanceof HTMLCanvasElement) {
|
|
621
|
+
try {
|
|
622
|
+
const replacement = document.createElement('img');
|
|
623
|
+
replacement.src = original.toDataURL('image/png');
|
|
624
|
+
replacement.alt = original.getAttribute('aria-label') || '';
|
|
625
|
+
replacement.setAttribute('style', original.getAttribute('style') || '');
|
|
626
|
+
clone.replaceWith(replacement);
|
|
627
|
+
} catch {
|
|
628
|
+
// Keep the canvas element when its pixels cannot be exported.
|
|
629
|
+
}
|
|
630
|
+
} else if (original instanceof HTMLVideoElement && original.readyState >= 2) {
|
|
631
|
+
try {
|
|
632
|
+
const replacement = document.createElement('img');
|
|
633
|
+
replacement.src = imageToDataUrl(original);
|
|
634
|
+
replacement.alt = original.getAttribute('aria-label') || '';
|
|
635
|
+
replacement.setAttribute('style', original.getAttribute('style') || '');
|
|
636
|
+
clone.replaceWith(replacement);
|
|
637
|
+
} catch {
|
|
638
|
+
clone.setAttribute('poster', original.poster || '');
|
|
639
|
+
}
|
|
640
|
+
} else if (original instanceof HTMLImageElement && original.complete) {
|
|
641
|
+
try {
|
|
642
|
+
clone.setAttribute('src', imageToDataUrl(original) || original.currentSrc || original.src);
|
|
643
|
+
clone.removeAttribute('srcset');
|
|
644
|
+
} catch {
|
|
645
|
+
clone.setAttribute('src', original.currentSrc || original.src);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const stylesheetLinks = [...document.querySelectorAll('link[rel="stylesheet"]')];
|
|
651
|
+
const clonedStylesheetLinks = [...clonedRoot.querySelectorAll('link[rel="stylesheet"]')];
|
|
652
|
+
|
|
653
|
+
await Promise.all(stylesheetLinks.map(async (link, index) => {
|
|
654
|
+
try {
|
|
655
|
+
const response = await fetch(link.href);
|
|
656
|
+
if (!response.ok) return;
|
|
657
|
+
|
|
658
|
+
const cssText = (await response.text()).replace(
|
|
659
|
+
/url\\(\\s*(['"]?)(?!data:|blob:|#)([^'"\\)]+)\\1\\s*\\)/gi,
|
|
660
|
+
(match, quote, resourceUrl) => {
|
|
661
|
+
try {
|
|
662
|
+
return 'url("' + new URL(resourceUrl.trim(), link.href).href + '")';
|
|
663
|
+
} catch {
|
|
664
|
+
return match;
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
);
|
|
668
|
+
const style = document.createElement('style');
|
|
669
|
+
style.textContent = cssText;
|
|
670
|
+
clonedStylesheetLinks[index].replaceWith(style);
|
|
671
|
+
} catch {
|
|
672
|
+
clonedStylesheetLinks[index].href = link.href;
|
|
673
|
+
}
|
|
674
|
+
}));
|
|
675
|
+
|
|
676
|
+
clonedRoot.querySelectorAll('script, link[rel="modulepreload"]').forEach((element) => element.remove());
|
|
677
|
+
clonedRoot.querySelectorAll('meta[http-equiv="Content-Security-Policy"]').forEach(
|
|
678
|
+
(element) => element.remove(),
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
const base = document.createElement('base');
|
|
682
|
+
base.href = location.href;
|
|
683
|
+
clonedRoot.querySelector('head').prepend(base);
|
|
684
|
+
|
|
685
|
+
const replayStyle = document.createElement('style');
|
|
686
|
+
replayStyle.textContent = [
|
|
687
|
+
'*,*::before,*::after{animation-play-state:paused!important;}',
|
|
688
|
+
'*{scrollbar-width:none!important;}',
|
|
689
|
+
'*::-webkit-scrollbar{width:0!important;height:0!important;display:none!important;}',
|
|
690
|
+
].join('');
|
|
691
|
+
clonedRoot.querySelector('head').append(replayStyle);
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
html: '<!doctype html>' + clonedRoot.outerHTML,
|
|
695
|
+
scrollX,
|
|
696
|
+
scrollY,
|
|
697
|
+
};
|
|
698
|
+
})()`,
|
|
699
|
+
awaitPromise: true,
|
|
700
|
+
returnByValue: true,
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
if (evaluation.exceptionDetails) {
|
|
704
|
+
throw new Error('The current WebView page could not be serialized.');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return evaluation.result.value;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function findBrowserExecutable() {
|
|
711
|
+
const executablePath = browserExecutablePaths.find((candidate) => (
|
|
712
|
+
fs.existsSync(candidate)
|
|
713
|
+
));
|
|
714
|
+
|
|
715
|
+
if (!executablePath) {
|
|
716
|
+
throw new Error('Google Chrome or Microsoft Edge was not found on this computer.');
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return executablePath;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function delay(milliseconds) {
|
|
723
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function launchReplayBrowser() {
|
|
727
|
+
fs.mkdirSync(temporaryDirectory, { recursive: true });
|
|
728
|
+
|
|
729
|
+
const profileDirectory = path.join(
|
|
730
|
+
temporaryDirectory,
|
|
731
|
+
`responsive-screenshot-chrome-${process.pid}-${Date.now()}`,
|
|
732
|
+
);
|
|
733
|
+
fs.mkdirSync(profileDirectory, { recursive: true });
|
|
734
|
+
|
|
735
|
+
const browserProcess = spawn(findBrowserExecutable(), [
|
|
736
|
+
'--headless=new',
|
|
737
|
+
'--remote-debugging-port=0',
|
|
738
|
+
`--user-data-dir=${profileDirectory}`,
|
|
739
|
+
'--no-first-run',
|
|
740
|
+
'--no-default-browser-check',
|
|
741
|
+
'--disable-background-networking',
|
|
742
|
+
'--remote-allow-origins=*',
|
|
743
|
+
'about:blank',
|
|
744
|
+
], {
|
|
745
|
+
stdio: 'ignore',
|
|
746
|
+
windowsHide: true,
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
const activePortPath = path.join(profileDirectory, 'DevToolsActivePort');
|
|
750
|
+
const deadline = Date.now() + 15000;
|
|
751
|
+
|
|
752
|
+
while (!fs.existsSync(activePortPath)) {
|
|
753
|
+
if (browserProcess.exitCode !== null) {
|
|
754
|
+
throw new Error('The local replay browser closed before it was ready.');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (Date.now() >= deadline) {
|
|
758
|
+
browserProcess.kill();
|
|
759
|
+
throw new Error('Timed out while starting the local replay browser.');
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
await delay(100);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const [port] = fs.readFileSync(activePortPath, 'utf8').trim().split(/\r?\n/);
|
|
766
|
+
const versionResponse = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
767
|
+
const version = await versionResponse.json();
|
|
768
|
+
const client = new CdpClient(version.webSocketDebuggerUrl);
|
|
769
|
+
await client.connect();
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
browserProcess,
|
|
773
|
+
client,
|
|
774
|
+
port,
|
|
775
|
+
profileDirectory,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function createReplayPage(browser) {
|
|
780
|
+
const createdTarget = await browser.client.send('Target.createTarget', {
|
|
781
|
+
url: 'about:blank',
|
|
782
|
+
});
|
|
783
|
+
const deadline = Date.now() + 10000;
|
|
784
|
+
|
|
785
|
+
while (Date.now() < deadline) {
|
|
786
|
+
const response = await fetch(`http://127.0.0.1:${browser.port}/json/list`);
|
|
787
|
+
const targets = await response.json();
|
|
788
|
+
const target = targets.find((candidate) => candidate.id === createdTarget.targetId);
|
|
789
|
+
|
|
790
|
+
if (target && target.webSocketDebuggerUrl) {
|
|
791
|
+
const client = new CdpClient(target.webSocketDebuggerUrl);
|
|
792
|
+
await client.connect();
|
|
793
|
+
await client.send('Page.enable');
|
|
794
|
+
await client.send('Runtime.enable');
|
|
795
|
+
return client;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
await delay(100);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
throw new Error('The local replay page did not become available.');
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function removeTemporaryBrowserProfile(profileDirectory) {
|
|
805
|
+
const resolvedTemporaryRoot = path.resolve(temporaryDirectory);
|
|
806
|
+
const resolvedProfileDirectory = path.resolve(profileDirectory);
|
|
807
|
+
|
|
808
|
+
if (!resolvedProfileDirectory.startsWith(`${resolvedTemporaryRoot}${path.sep}`)) {
|
|
809
|
+
throw new Error('Refusing to remove a browser profile outside agent-temp.');
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
fs.rmSync(resolvedProfileDirectory, { recursive: true, force: true });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async function closeReplayBrowser(browser) {
|
|
816
|
+
try {
|
|
817
|
+
await browser.client.send('Browser.close');
|
|
818
|
+
} catch {
|
|
819
|
+
// Closing the browser can close the protocol connection before it acknowledges.
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const deadline = Date.now() + 3000;
|
|
823
|
+
|
|
824
|
+
while (browser.browserProcess.exitCode === null && Date.now() < deadline) {
|
|
825
|
+
await delay(100);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (browser.browserProcess.exitCode === null) {
|
|
829
|
+
browser.browserProcess.kill();
|
|
830
|
+
await delay(200);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
browser.client.close();
|
|
834
|
+
|
|
835
|
+
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
836
|
+
try {
|
|
837
|
+
removeTemporaryBrowserProfile(browser.profileDirectory);
|
|
838
|
+
return;
|
|
839
|
+
} catch (error) {
|
|
840
|
+
if (attempt === 5) {
|
|
841
|
+
throw error;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
await delay(200);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async function waitForReplayLayout(client, snapshot) {
|
|
850
|
+
await client.send('Runtime.evaluate', {
|
|
851
|
+
expression: `(async () => {
|
|
852
|
+
const assetWait = Promise.all([...document.images].map((image) => {
|
|
853
|
+
if (image.complete) return Promise.resolve();
|
|
854
|
+
return new Promise((resolve) => {
|
|
855
|
+
image.addEventListener('load', resolve, { once: true });
|
|
856
|
+
image.addEventListener('error', resolve, { once: true });
|
|
857
|
+
});
|
|
858
|
+
}));
|
|
859
|
+
|
|
860
|
+
await Promise.race([
|
|
861
|
+
Promise.all([document.fonts.ready, assetWait]),
|
|
862
|
+
new Promise((resolve) => setTimeout(resolve, 5000)),
|
|
863
|
+
]);
|
|
864
|
+
|
|
865
|
+
document.querySelectorAll('[data-responsive-scroll-top]').forEach((element) => {
|
|
866
|
+
element.scrollTop = Number(element.dataset.responsiveScrollTop);
|
|
867
|
+
element.scrollLeft = Number(element.dataset.responsiveScrollLeft);
|
|
868
|
+
});
|
|
869
|
+
scrollTo(${Number(snapshot.scrollX) || 0}, ${Number(snapshot.scrollY) || 0});
|
|
870
|
+
|
|
871
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
872
|
+
})()`,
|
|
873
|
+
awaitPromise: true,
|
|
874
|
+
returnByValue: true,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
869
878
|
function getFrameAdornment(frameType) {
|
|
870
|
-
if (frameType === 'iphone') {
|
|
871
|
-
return '<div class="dynamic-island"></div>';
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
if (frameType === 'android-phone') {
|
|
875
|
-
return '<div class="camera-dot"></div>';
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
if (frameType === 'tablet') {
|
|
879
|
-
return '<div class="tablet-camera"></div>';
|
|
880
|
-
}
|
|
881
|
-
|
|
879
|
+
if (frameType === 'iphone') {
|
|
880
|
+
return '<div class="dynamic-island"></div>';
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (frameType === 'android-phone') {
|
|
884
|
+
return '<div class="camera-dot"></div>';
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (frameType === 'tablet') {
|
|
888
|
+
return '<div class="tablet-camera"></div>';
|
|
889
|
+
}
|
|
890
|
+
|
|
882
891
|
if (frameType === 'tv' || frameType === 'apple-tv') {
|
|
883
892
|
return '<div class="tv-stand"></div>';
|
|
884
893
|
}
|
|
@@ -890,110 +899,110 @@ function getFrameAdornment(frameType) {
|
|
|
890
899
|
if (frameType === 'apple-vision-pro') {
|
|
891
900
|
return '<div class="vision-sensor vision-sensor-left"></div><div class="vision-sensor vision-sensor-right"></div>';
|
|
892
901
|
}
|
|
893
|
-
|
|
894
|
-
if (frameType === 'laptop') {
|
|
895
|
-
return '<div class="laptop-camera"></div><div class="laptop-base"></div>';
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
return '<div class="xr-status"></div>';
|
|
899
|
-
}
|
|
900
|
-
|
|
902
|
+
|
|
903
|
+
if (frameType === 'laptop') {
|
|
904
|
+
return '<div class="laptop-camera"></div><div class="laptop-base"></div>';
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return '<div class="xr-status"></div>';
|
|
908
|
+
}
|
|
909
|
+
|
|
901
910
|
function createDeviceFrameHtml(profile, screenshotBuffer) {
|
|
902
|
-
const screenshotDataUrl = `data:image/png;base64,${screenshotBuffer.toString('base64')}`;
|
|
903
|
-
const aspectRatio = `${profile.width} / ${profile.height}`;
|
|
904
|
-
|
|
905
|
-
return `<!doctype html>
|
|
906
|
-
<html>
|
|
907
|
-
<head>
|
|
908
|
-
<meta charset="utf-8" />
|
|
909
|
-
<style>
|
|
910
|
-
* { box-sizing: border-box; }
|
|
911
|
-
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
|
912
|
-
body {
|
|
913
|
-
display: grid;
|
|
914
|
-
place-items: center;
|
|
915
|
-
position: relative;
|
|
916
|
-
background:
|
|
917
|
-
radial-gradient(circle at 14% 18%, rgba(45, 213, 255, 0.24), transparent 30%),
|
|
918
|
-
radial-gradient(circle at 86% 82%, rgba(196, 255, 61, 0.16), transparent 28%),
|
|
919
|
-
linear-gradient(145deg, #111827 0%, #070b12 52%, #020409 100%);
|
|
920
|
-
font-family: Arial, sans-serif;
|
|
921
|
-
}
|
|
922
|
-
body::before {
|
|
923
|
-
content: '';
|
|
924
|
-
position: absolute;
|
|
925
|
-
inset: 0;
|
|
926
|
-
opacity: 0.18;
|
|
927
|
-
background-image:
|
|
928
|
-
linear-gradient(rgba(255,255,255,0.045) 1px, transparent 1px),
|
|
929
|
-
linear-gradient(90deg, rgba(255,255,255,0.045) 1px, transparent 1px);
|
|
930
|
-
background-size: 7vw 7vw;
|
|
931
|
-
mask-image: linear-gradient(to bottom, transparent, black 28%, black 72%, transparent);
|
|
932
|
-
}
|
|
933
|
-
.device {
|
|
934
|
-
position: relative;
|
|
935
|
-
z-index: 1;
|
|
936
|
-
aspect-ratio: ${aspectRatio};
|
|
937
|
-
background: linear-gradient(145deg, #566171 0%, #171c25 18%, #05070b 55%, #384252 100%);
|
|
938
|
-
border: clamp(2px, 0.45vw, 9px) solid #727d8d;
|
|
939
|
-
box-shadow:
|
|
940
|
-
0 4vh 9vh rgba(0, 0, 0, 0.68),
|
|
941
|
-
0 0 0 1px rgba(255, 255, 255, 0.18) inset,
|
|
942
|
-
0 0 5vh rgba(45, 213, 255, 0.12);
|
|
943
|
-
}
|
|
944
|
-
.screen {
|
|
945
|
-
position: relative;
|
|
946
|
-
width: 100%;
|
|
947
|
-
height: 100%;
|
|
948
|
-
overflow: hidden;
|
|
949
|
-
background: #05070b;
|
|
950
|
-
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.95) inset;
|
|
951
|
-
}
|
|
952
|
-
.screen img {
|
|
953
|
-
display: block;
|
|
954
|
-
width: 100%;
|
|
955
|
-
height: 100%;
|
|
956
|
-
object-fit: cover;
|
|
957
|
-
}
|
|
958
|
-
.device.android-phone,
|
|
959
|
-
.device.iphone {
|
|
960
|
-
height: 87%;
|
|
961
|
-
padding: 1.25%;
|
|
962
|
-
border-radius: clamp(24px, 8vw, 70px);
|
|
963
|
-
}
|
|
964
|
-
.device.android-phone .screen,
|
|
965
|
-
.device.iphone .screen {
|
|
966
|
-
border-radius: clamp(19px, 6.7vw, 60px);
|
|
967
|
-
}
|
|
968
|
-
.camera-dot {
|
|
969
|
-
position: absolute;
|
|
970
|
-
z-index: 3;
|
|
971
|
-
top: 2.1%;
|
|
972
|
-
left: 50%;
|
|
973
|
-
width: clamp(5px, 1.8vw, 16px);
|
|
974
|
-
aspect-ratio: 1;
|
|
975
|
-
transform: translateX(-50%);
|
|
976
|
-
border-radius: 50%;
|
|
977
|
-
background: radial-gradient(circle at 36% 34%, #406891, #080b10 46%, #000 72%);
|
|
978
|
-
box-shadow: 0 0 0 2px #111827;
|
|
979
|
-
}
|
|
980
|
-
.dynamic-island {
|
|
981
|
-
position: absolute;
|
|
982
|
-
z-index: 3;
|
|
983
|
-
top: 2.2%;
|
|
984
|
-
left: 50%;
|
|
985
|
-
width: 29%;
|
|
986
|
-
height: 3.3%;
|
|
987
|
-
transform: translateX(-50%);
|
|
988
|
-
border-radius: 999px;
|
|
989
|
-
background: #000;
|
|
990
|
-
box-shadow: 0 0 0 1px rgba(255,255,255,0.05);
|
|
991
|
-
}
|
|
992
|
-
.device.tablet {
|
|
993
|
-
height: 86%;
|
|
994
|
-
padding: 1.05%;
|
|
995
|
-
border-radius: clamp(18px, 3.2vw, 48px);
|
|
996
|
-
}
|
|
911
|
+
const screenshotDataUrl = `data:image/png;base64,${screenshotBuffer.toString('base64')}`;
|
|
912
|
+
const aspectRatio = `${profile.width} / ${profile.height}`;
|
|
913
|
+
|
|
914
|
+
return `<!doctype html>
|
|
915
|
+
<html>
|
|
916
|
+
<head>
|
|
917
|
+
<meta charset="utf-8" />
|
|
918
|
+
<style>
|
|
919
|
+
* { box-sizing: border-box; }
|
|
920
|
+
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
|
921
|
+
body {
|
|
922
|
+
display: grid;
|
|
923
|
+
place-items: center;
|
|
924
|
+
position: relative;
|
|
925
|
+
background:
|
|
926
|
+
radial-gradient(circle at 14% 18%, rgba(45, 213, 255, 0.24), transparent 30%),
|
|
927
|
+
radial-gradient(circle at 86% 82%, rgba(196, 255, 61, 0.16), transparent 28%),
|
|
928
|
+
linear-gradient(145deg, #111827 0%, #070b12 52%, #020409 100%);
|
|
929
|
+
font-family: Arial, sans-serif;
|
|
930
|
+
}
|
|
931
|
+
body::before {
|
|
932
|
+
content: '';
|
|
933
|
+
position: absolute;
|
|
934
|
+
inset: 0;
|
|
935
|
+
opacity: 0.18;
|
|
936
|
+
background-image:
|
|
937
|
+
linear-gradient(rgba(255,255,255,0.045) 1px, transparent 1px),
|
|
938
|
+
linear-gradient(90deg, rgba(255,255,255,0.045) 1px, transparent 1px);
|
|
939
|
+
background-size: 7vw 7vw;
|
|
940
|
+
mask-image: linear-gradient(to bottom, transparent, black 28%, black 72%, transparent);
|
|
941
|
+
}
|
|
942
|
+
.device {
|
|
943
|
+
position: relative;
|
|
944
|
+
z-index: 1;
|
|
945
|
+
aspect-ratio: ${aspectRatio};
|
|
946
|
+
background: linear-gradient(145deg, #566171 0%, #171c25 18%, #05070b 55%, #384252 100%);
|
|
947
|
+
border: clamp(2px, 0.45vw, 9px) solid #727d8d;
|
|
948
|
+
box-shadow:
|
|
949
|
+
0 4vh 9vh rgba(0, 0, 0, 0.68),
|
|
950
|
+
0 0 0 1px rgba(255, 255, 255, 0.18) inset,
|
|
951
|
+
0 0 5vh rgba(45, 213, 255, 0.12);
|
|
952
|
+
}
|
|
953
|
+
.screen {
|
|
954
|
+
position: relative;
|
|
955
|
+
width: 100%;
|
|
956
|
+
height: 100%;
|
|
957
|
+
overflow: hidden;
|
|
958
|
+
background: #05070b;
|
|
959
|
+
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.95) inset;
|
|
960
|
+
}
|
|
961
|
+
.screen img {
|
|
962
|
+
display: block;
|
|
963
|
+
width: 100%;
|
|
964
|
+
height: 100%;
|
|
965
|
+
object-fit: cover;
|
|
966
|
+
}
|
|
967
|
+
.device.android-phone,
|
|
968
|
+
.device.iphone {
|
|
969
|
+
height: 87%;
|
|
970
|
+
padding: 1.25%;
|
|
971
|
+
border-radius: clamp(24px, 8vw, 70px);
|
|
972
|
+
}
|
|
973
|
+
.device.android-phone .screen,
|
|
974
|
+
.device.iphone .screen {
|
|
975
|
+
border-radius: clamp(19px, 6.7vw, 60px);
|
|
976
|
+
}
|
|
977
|
+
.camera-dot {
|
|
978
|
+
position: absolute;
|
|
979
|
+
z-index: 3;
|
|
980
|
+
top: 2.1%;
|
|
981
|
+
left: 50%;
|
|
982
|
+
width: clamp(5px, 1.8vw, 16px);
|
|
983
|
+
aspect-ratio: 1;
|
|
984
|
+
transform: translateX(-50%);
|
|
985
|
+
border-radius: 50%;
|
|
986
|
+
background: radial-gradient(circle at 36% 34%, #406891, #080b10 46%, #000 72%);
|
|
987
|
+
box-shadow: 0 0 0 2px #111827;
|
|
988
|
+
}
|
|
989
|
+
.dynamic-island {
|
|
990
|
+
position: absolute;
|
|
991
|
+
z-index: 3;
|
|
992
|
+
top: 2.2%;
|
|
993
|
+
left: 50%;
|
|
994
|
+
width: 29%;
|
|
995
|
+
height: 3.3%;
|
|
996
|
+
transform: translateX(-50%);
|
|
997
|
+
border-radius: 999px;
|
|
998
|
+
background: #000;
|
|
999
|
+
box-shadow: 0 0 0 1px rgba(255,255,255,0.05);
|
|
1000
|
+
}
|
|
1001
|
+
.device.tablet {
|
|
1002
|
+
height: 86%;
|
|
1003
|
+
padding: 1.05%;
|
|
1004
|
+
border-radius: clamp(18px, 3.2vw, 48px);
|
|
1005
|
+
}
|
|
997
1006
|
.device.tablet .screen {
|
|
998
1007
|
border-radius: clamp(13px, 2.5vw, 38px);
|
|
999
1008
|
}
|
|
@@ -1052,90 +1061,90 @@ function createDeviceFrameHtml(profile, screenshotBuffer) {
|
|
|
1052
1061
|
}
|
|
1053
1062
|
.vision-sensor-left { left: 3.2%; }
|
|
1054
1063
|
.vision-sensor-right { right: 3.2%; }
|
|
1055
|
-
.tablet-camera,
|
|
1056
|
-
.laptop-camera {
|
|
1057
|
-
position: absolute;
|
|
1058
|
-
z-index: 3;
|
|
1059
|
-
top: 0.48%;
|
|
1060
|
-
left: 50%;
|
|
1061
|
-
width: clamp(3px, 0.55vw, 10px);
|
|
1062
|
-
aspect-ratio: 1;
|
|
1063
|
-
transform: translateX(-50%);
|
|
1064
|
-
border-radius: 50%;
|
|
1065
|
-
background: #080b10;
|
|
1066
|
-
box-shadow: 0 0 0 1px #526071;
|
|
1067
|
-
}
|
|
1064
|
+
.tablet-camera,
|
|
1065
|
+
.laptop-camera {
|
|
1066
|
+
position: absolute;
|
|
1067
|
+
z-index: 3;
|
|
1068
|
+
top: 0.48%;
|
|
1069
|
+
left: 50%;
|
|
1070
|
+
width: clamp(3px, 0.55vw, 10px);
|
|
1071
|
+
aspect-ratio: 1;
|
|
1072
|
+
transform: translateX(-50%);
|
|
1073
|
+
border-radius: 50%;
|
|
1074
|
+
background: #080b10;
|
|
1075
|
+
box-shadow: 0 0 0 1px #526071;
|
|
1076
|
+
}
|
|
1068
1077
|
.device.tv,
|
|
1069
1078
|
.device.apple-tv {
|
|
1070
1079
|
width: 82%;
|
|
1071
|
-
padding: 0.62%;
|
|
1072
|
-
border-radius: clamp(8px, 1.1vw, 24px);
|
|
1073
|
-
transform: translateY(-4%);
|
|
1074
|
-
}
|
|
1080
|
+
padding: 0.62%;
|
|
1081
|
+
border-radius: clamp(8px, 1.1vw, 24px);
|
|
1082
|
+
transform: translateY(-4%);
|
|
1083
|
+
}
|
|
1075
1084
|
.device.tv .screen,
|
|
1076
1085
|
.device.apple-tv .screen { border-radius: clamp(4px, 0.55vw, 12px); }
|
|
1077
|
-
.tv-stand {
|
|
1078
|
-
position: absolute;
|
|
1079
|
-
z-index: -1;
|
|
1080
|
-
left: 50%;
|
|
1081
|
-
bottom: -13%;
|
|
1082
|
-
width: 24%;
|
|
1083
|
-
height: 13%;
|
|
1084
|
-
transform: translateX(-50%);
|
|
1085
|
-
background: linear-gradient(90deg, #11161f, #697383 50%, #11161f);
|
|
1086
|
-
clip-path: polygon(42% 0, 58% 0, 67% 76%, 94% 86%, 100% 100%, 0 100%, 6% 86%, 33% 76%);
|
|
1087
|
-
filter: drop-shadow(0 1.5vh 1.5vh rgba(0,0,0,0.55));
|
|
1088
|
-
}
|
|
1089
|
-
.device.laptop {
|
|
1090
|
-
width: 82%;
|
|
1091
|
-
padding: 0.75% 0.75% 1.15%;
|
|
1092
|
-
border-radius: clamp(8px, 1.2vw, 24px);
|
|
1093
|
-
transform: translateY(-3%);
|
|
1094
|
-
}
|
|
1095
|
-
.device.laptop .screen { border-radius: clamp(4px, 0.55vw, 12px); }
|
|
1096
|
-
.laptop-base {
|
|
1097
|
-
position: absolute;
|
|
1098
|
-
z-index: -1;
|
|
1099
|
-
left: 50%;
|
|
1100
|
-
bottom: -7.5%;
|
|
1101
|
-
width: 112%;
|
|
1102
|
-
height: 8%;
|
|
1103
|
-
transform: translateX(-50%);
|
|
1104
|
-
border-radius: 0 0 50% 50% / 0 0 70% 70%;
|
|
1105
|
-
background: linear-gradient(#697383, #202631 45%, #090c11);
|
|
1106
|
-
box-shadow: 0 1.5vh 2vh rgba(0,0,0,0.48);
|
|
1107
|
-
}
|
|
1108
|
-
.device.xr {
|
|
1109
|
-
width: 82%;
|
|
1110
|
-
padding: 0.58%;
|
|
1111
|
-
border: clamp(2px, 0.32vw, 7px) solid rgba(128, 230, 255, 0.9);
|
|
1112
|
-
border-radius: clamp(18px, 2.5vw, 48px);
|
|
1113
|
-
background: rgba(13, 22, 34, 0.82);
|
|
1114
|
-
box-shadow:
|
|
1115
|
-
0 4vh 10vh rgba(0,0,0,0.65),
|
|
1116
|
-
0 0 7vh rgba(45,213,255,0.34),
|
|
1117
|
-
0 0 0 1px rgba(255,255,255,0.22) inset;
|
|
1118
|
-
}
|
|
1119
|
-
.device.xr .screen { border-radius: clamp(13px, 2vw, 38px); }
|
|
1120
|
-
.xr-status {
|
|
1121
|
-
position: absolute;
|
|
1122
|
-
top: -2.4%;
|
|
1123
|
-
left: 50%;
|
|
1124
|
-
width: 13%;
|
|
1125
|
-
height: 1.1%;
|
|
1126
|
-
transform: translateX(-50%);
|
|
1127
|
-
border-radius: 999px;
|
|
1128
|
-
background: #6fe8ff;
|
|
1129
|
-
box-shadow: 0 0 1.8vh #2dd5ff;
|
|
1130
|
-
}
|
|
1131
|
-
</style>
|
|
1132
|
-
</head>
|
|
1133
|
-
<body>
|
|
1134
|
-
<main class="device ${profile.frameType}">
|
|
1135
|
-
<div class="screen"><img src="${screenshotDataUrl}" /></div>
|
|
1136
|
-
${getFrameAdornment(profile.frameType)}
|
|
1137
|
-
</main>
|
|
1138
|
-
</body>
|
|
1086
|
+
.tv-stand {
|
|
1087
|
+
position: absolute;
|
|
1088
|
+
z-index: -1;
|
|
1089
|
+
left: 50%;
|
|
1090
|
+
bottom: -13%;
|
|
1091
|
+
width: 24%;
|
|
1092
|
+
height: 13%;
|
|
1093
|
+
transform: translateX(-50%);
|
|
1094
|
+
background: linear-gradient(90deg, #11161f, #697383 50%, #11161f);
|
|
1095
|
+
clip-path: polygon(42% 0, 58% 0, 67% 76%, 94% 86%, 100% 100%, 0 100%, 6% 86%, 33% 76%);
|
|
1096
|
+
filter: drop-shadow(0 1.5vh 1.5vh rgba(0,0,0,0.55));
|
|
1097
|
+
}
|
|
1098
|
+
.device.laptop {
|
|
1099
|
+
width: 82%;
|
|
1100
|
+
padding: 0.75% 0.75% 1.15%;
|
|
1101
|
+
border-radius: clamp(8px, 1.2vw, 24px);
|
|
1102
|
+
transform: translateY(-3%);
|
|
1103
|
+
}
|
|
1104
|
+
.device.laptop .screen { border-radius: clamp(4px, 0.55vw, 12px); }
|
|
1105
|
+
.laptop-base {
|
|
1106
|
+
position: absolute;
|
|
1107
|
+
z-index: -1;
|
|
1108
|
+
left: 50%;
|
|
1109
|
+
bottom: -7.5%;
|
|
1110
|
+
width: 112%;
|
|
1111
|
+
height: 8%;
|
|
1112
|
+
transform: translateX(-50%);
|
|
1113
|
+
border-radius: 0 0 50% 50% / 0 0 70% 70%;
|
|
1114
|
+
background: linear-gradient(#697383, #202631 45%, #090c11);
|
|
1115
|
+
box-shadow: 0 1.5vh 2vh rgba(0,0,0,0.48);
|
|
1116
|
+
}
|
|
1117
|
+
.device.xr {
|
|
1118
|
+
width: 82%;
|
|
1119
|
+
padding: 0.58%;
|
|
1120
|
+
border: clamp(2px, 0.32vw, 7px) solid rgba(128, 230, 255, 0.9);
|
|
1121
|
+
border-radius: clamp(18px, 2.5vw, 48px);
|
|
1122
|
+
background: rgba(13, 22, 34, 0.82);
|
|
1123
|
+
box-shadow:
|
|
1124
|
+
0 4vh 10vh rgba(0,0,0,0.65),
|
|
1125
|
+
0 0 7vh rgba(45,213,255,0.34),
|
|
1126
|
+
0 0 0 1px rgba(255,255,255,0.22) inset;
|
|
1127
|
+
}
|
|
1128
|
+
.device.xr .screen { border-radius: clamp(13px, 2vw, 38px); }
|
|
1129
|
+
.xr-status {
|
|
1130
|
+
position: absolute;
|
|
1131
|
+
top: -2.4%;
|
|
1132
|
+
left: 50%;
|
|
1133
|
+
width: 13%;
|
|
1134
|
+
height: 1.1%;
|
|
1135
|
+
transform: translateX(-50%);
|
|
1136
|
+
border-radius: 999px;
|
|
1137
|
+
background: #6fe8ff;
|
|
1138
|
+
box-shadow: 0 0 1.8vh #2dd5ff;
|
|
1139
|
+
}
|
|
1140
|
+
</style>
|
|
1141
|
+
</head>
|
|
1142
|
+
<body>
|
|
1143
|
+
<main class="device ${profile.frameType}">
|
|
1144
|
+
<div class="screen"><img src="${screenshotDataUrl}" /></div>
|
|
1145
|
+
${getFrameAdornment(profile.frameType)}
|
|
1146
|
+
</main>
|
|
1147
|
+
</body>
|
|
1139
1148
|
</html>`;
|
|
1140
1149
|
}
|
|
1141
1150
|
|
|
@@ -1148,17 +1157,147 @@ function escapeHtml(value) {
|
|
|
1148
1157
|
.replace(/'/g, ''');
|
|
1149
1158
|
}
|
|
1150
1159
|
|
|
1151
|
-
function
|
|
1160
|
+
function cleanAssetCopy(value, maximumLength, fallback = '') {
|
|
1161
|
+
const cleaned = String(value || '')
|
|
1162
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
|
|
1163
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
|
|
1164
|
+
.replace(/<[^>]+>/g, ' ')
|
|
1165
|
+
.replace(/&[a-zA-Z#0-9]+;/g, ' ')
|
|
1166
|
+
.replace(/[_|]+/g, ' ')
|
|
1167
|
+
.replace(/\s+/g, ' ')
|
|
1168
|
+
.trim();
|
|
1169
|
+
if (!cleaned) return fallback;
|
|
1170
|
+
if (cleaned.length <= maximumLength) return cleaned;
|
|
1171
|
+
const shortened = cleaned.slice(0, maximumLength + 1);
|
|
1172
|
+
const lastSpace = shortened.lastIndexOf(' ');
|
|
1173
|
+
return shortened.slice(0, lastSpace > maximumLength * 0.55 ? lastSpace : maximumLength).trim();
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function readJsonAssetSource(filePath) {
|
|
1177
|
+
try {
|
|
1178
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
1179
|
+
} catch {
|
|
1180
|
+
return {};
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function collectProjectUiPhrases() {
|
|
1185
|
+
const phrases = [];
|
|
1186
|
+
const pageDirectory = path.join(__dirname, 'src', 'pages');
|
|
1187
|
+
if (fs.existsSync(pageDirectory)) {
|
|
1188
|
+
const pageFiles = fs.readdirSync(pageDirectory)
|
|
1189
|
+
.filter((fileName) => /\.(?:f7|html)$/i.test(fileName))
|
|
1190
|
+
.sort()
|
|
1191
|
+
.slice(0, 40);
|
|
1192
|
+
const visibleElementPattern = /<(?:h[1-4]|button|label|option|a|li)\b[^>]*>([\s\S]*?)<\/(?:h[1-4]|button|label|option|a|li)>/gi;
|
|
1193
|
+
for (const fileName of pageFiles) {
|
|
1194
|
+
const source = fs.readFileSync(path.join(pageDirectory, fileName), 'utf8');
|
|
1195
|
+
let match;
|
|
1196
|
+
while ((match = visibleElementPattern.exec(source)) && phrases.length < 120) {
|
|
1197
|
+
const phrase = cleanAssetCopy(match[1], 52);
|
|
1198
|
+
if (phrase) phrases.push(phrase);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return phrases;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function getStoreAssetCopy(config) {
|
|
1206
|
+
const configuredAppName = cleanAssetCopy(config.appName, 40, 'App');
|
|
1207
|
+
const appUniqueId = String(config.android?.APP_UNIQUE_ID ?? '').trim();
|
|
1208
|
+
const promoCopyPath = appUniqueId
|
|
1209
|
+
? path.join(
|
|
1210
|
+
__dirname,
|
|
1211
|
+
'Auto-AI-Video',
|
|
1212
|
+
`${appUniqueId}. ${sanitizeName(configuredAppName, 'App')}-promo-copy.json`,
|
|
1213
|
+
)
|
|
1214
|
+
: '';
|
|
1215
|
+
const promoCopy = promoCopyPath && fs.existsSync(promoCopyPath)
|
|
1216
|
+
? readJsonAssetSource(promoCopyPath).copy || {}
|
|
1217
|
+
: {};
|
|
1218
|
+
const scenes = Array.isArray(promoCopy.scenes) ? promoCopy.scenes : [];
|
|
1219
|
+
const footerKeywords = Array.isArray(promoCopy.footer_keywords)
|
|
1220
|
+
? promoCopy.footer_keywords
|
|
1221
|
+
: [];
|
|
1222
|
+
const manifestCandidates = [
|
|
1223
|
+
path.join(__dirname, 'src', 'manifest.webmanifest'),
|
|
1224
|
+
path.join(__dirname, 'public', 'manifest.webmanifest'),
|
|
1225
|
+
path.join(__dirname, 'manifest.webmanifest'),
|
|
1226
|
+
];
|
|
1227
|
+
const manifestPath = manifestCandidates.find((candidate) => fs.existsSync(candidate));
|
|
1228
|
+
const manifest = manifestPath ? readJsonAssetSource(manifestPath) : {};
|
|
1229
|
+
const candidates = [
|
|
1230
|
+
...footerKeywords,
|
|
1231
|
+
...scenes.flatMap((scene) => [
|
|
1232
|
+
`${scene.title_line_1 || ''} ${scene.title_line_2 || ''}`,
|
|
1233
|
+
scene.kicker,
|
|
1234
|
+
scene.body,
|
|
1235
|
+
]),
|
|
1236
|
+
...(Array.isArray(manifest.categories) ? manifest.categories : []),
|
|
1237
|
+
manifest.description,
|
|
1238
|
+
...collectProjectUiPhrases(),
|
|
1239
|
+
];
|
|
1240
|
+
const uniquePhrases = [];
|
|
1241
|
+
const seen = new Set([configuredAppName.toLowerCase()]);
|
|
1242
|
+
for (const candidate of candidates) {
|
|
1243
|
+
const phrase = cleanAssetCopy(candidate, 52);
|
|
1244
|
+
const key = phrase.toLowerCase();
|
|
1245
|
+
if (phrase.length < 3 || seen.has(key)) continue;
|
|
1246
|
+
seen.add(key);
|
|
1247
|
+
uniquePhrases.push(phrase);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const featureLabels = uniquePhrases
|
|
1251
|
+
.map((phrase) => cleanAssetCopy(phrase, 18))
|
|
1252
|
+
.filter((phrase, index, values) => (
|
|
1253
|
+
phrase.length >= 3
|
|
1254
|
+
&& values.findIndex((value) => value.toLowerCase() === phrase.toLowerCase()) === index
|
|
1255
|
+
))
|
|
1256
|
+
.slice(0, 9);
|
|
1257
|
+
while (featureLabels.length < 9) {
|
|
1258
|
+
featureLabels.push(`Feature ${String(featureLabels.length + 1).padStart(2, '0')}`);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const firstScene = scenes[0] || {};
|
|
1262
|
+
const firstSceneTitle = cleanAssetCopy(
|
|
1263
|
+
`${firstScene.title_line_1 || ''} ${firstScene.title_line_2 || ''}`,
|
|
1264
|
+
34,
|
|
1265
|
+
);
|
|
1266
|
+
const primaryPhrase = footerKeywords[0] || featureLabels[0];
|
|
1267
|
+
return {
|
|
1268
|
+
amazonEyebrow: cleanAssetCopy(primaryPhrase, 32, `Explore ${configuredAppName}`),
|
|
1269
|
+
amazonCallout: cleanAssetCopy(
|
|
1270
|
+
firstScene.body || manifest.description || uniquePhrases[0],
|
|
1271
|
+
52,
|
|
1272
|
+
`Explore ${configuredAppName}`,
|
|
1273
|
+
),
|
|
1274
|
+
featureLabels,
|
|
1275
|
+
samsungKicker: cleanAssetCopy(firstScene.kicker || primaryPhrase, 28, 'App features'),
|
|
1276
|
+
samsungAction: firstSceneTitle || cleanAssetCopy(primaryPhrase, 34, `Open ${configuredAppName}`),
|
|
1277
|
+
samsungBody: cleanAssetCopy(
|
|
1278
|
+
firstScene.body || manifest.description || uniquePhrases[1],
|
|
1279
|
+
52,
|
|
1280
|
+
'Explore the available features',
|
|
1281
|
+
),
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function createAmazonAssetHtml(asset, appName, iconBuffer, storeCopy) {
|
|
1152
1286
|
const iconDataUrl = `data:image/png;base64,${iconBuffer.toString('base64')}`;
|
|
1153
1287
|
const safeAppName = escapeHtml(appName);
|
|
1288
|
+
const safeEyebrow = escapeHtml(storeCopy.amazonEyebrow);
|
|
1289
|
+
const safeCallout = escapeHtml(storeCopy.amazonCallout);
|
|
1290
|
+
const featureMarkup = storeCopy.featureLabels.slice(0, 6)
|
|
1291
|
+
.map((label) => `<span>${escapeHtml(label)}</span>`)
|
|
1292
|
+
.join('');
|
|
1154
1293
|
const logoMarkup = asset.type === 'featured-logo'
|
|
1155
|
-
? `<main class="featured-logo"><img src="${iconDataUrl}" /><div><small
|
|
1294
|
+
? `<main class="featured-logo"><img src="${iconDataUrl}" /><div><small>${safeEyebrow}</small><strong>${safeAppName}</strong></div></main>`
|
|
1156
1295
|
: '';
|
|
1157
1296
|
const iconMarkup = asset.type === 'icon'
|
|
1158
|
-
? `<main class="icon-card"><img src="${iconDataUrl}" /><div><small
|
|
1297
|
+
? `<main class="icon-card"><img src="${iconDataUrl}" /><div><small>${safeCallout}</small><strong>${safeAppName}</strong></div></main>`
|
|
1159
1298
|
: '';
|
|
1160
1299
|
const backgroundMarkup = asset.type.includes('background')
|
|
1161
|
-
?
|
|
1300
|
+
? `<main class="background-art"><div class="pulse pulse-one"></div><div class="pulse pulse-two"></div><div class="feature-grid">${featureMarkup}</div></main>`
|
|
1162
1301
|
: '';
|
|
1163
1302
|
|
|
1164
1303
|
return `<!doctype html>
|
|
@@ -1257,45 +1396,35 @@ function createAmazonAssetHtml(asset, appName, iconBuffer) {
|
|
|
1257
1396
|
}
|
|
1258
1397
|
.pulse-one { width: 520px; height: 520px; top: -180px; left: -110px; }
|
|
1259
1398
|
.pulse-two { width: 620px; height: 620px; right: -180px; bottom: -310px; border-color: rgba(201,255,65,0.22); }
|
|
1260
|
-
.
|
|
1399
|
+
.feature-grid {
|
|
1261
1400
|
position: absolute;
|
|
1262
1401
|
left: 8%;
|
|
1263
|
-
|
|
1264
|
-
display: flex;
|
|
1265
|
-
align-items: center;
|
|
1266
|
-
gap: 20px;
|
|
1267
|
-
height: 44%;
|
|
1268
|
-
transform: translateY(-50%);
|
|
1269
|
-
}
|
|
1270
|
-
.equalizer i { width: 18px; border-radius: 999px; background: #35d4eb; box-shadow: 0 0 25px rgba(53,212,235,0.5); }
|
|
1271
|
-
.equalizer i:nth-child(1), .equalizer i:nth-child(7) { height: 28%; }
|
|
1272
|
-
.equalizer i:nth-child(2), .equalizer i:nth-child(6) { height: 52%; background: #ff5964; }
|
|
1273
|
-
.equalizer i:nth-child(3), .equalizer i:nth-child(5) { height: 76%; background: #ff9f43; }
|
|
1274
|
-
.equalizer i:nth-child(4) { height: 100%; background: #c9ff41; }
|
|
1275
|
-
.pad-grid {
|
|
1276
|
-
position: absolute;
|
|
1277
|
-
right: 7%;
|
|
1402
|
+
right: 8%;
|
|
1278
1403
|
top: 50%;
|
|
1279
1404
|
display: grid;
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
gap: 20px;
|
|
1284
|
-
transform: translateY(-50%) perspective(900px) rotateY(-10deg) rotateX(5deg);
|
|
1405
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1406
|
+
gap: 24px;
|
|
1407
|
+
transform: translateY(-50%);
|
|
1285
1408
|
}
|
|
1286
|
-
.
|
|
1409
|
+
.feature-grid span {
|
|
1410
|
+
display: flex;
|
|
1411
|
+
min-height: 108px;
|
|
1412
|
+
align-items: center;
|
|
1413
|
+
justify-content: center;
|
|
1414
|
+
padding: 20px;
|
|
1415
|
+
color: #f8fbff;
|
|
1287
1416
|
border: 2px solid rgba(255,255,255,0.17);
|
|
1288
|
-
border-top: 7px solid #35d4eb;
|
|
1289
1417
|
border-radius: 22px;
|
|
1290
1418
|
background: linear-gradient(145deg, rgba(42,55,75,0.96), rgba(11,18,29,0.96));
|
|
1291
1419
|
box-shadow: 0 20px 28px rgba(0,0,0,0.28);
|
|
1420
|
+
font-size: clamp(20px, 2vw, 34px);
|
|
1421
|
+
font-weight: 700;
|
|
1422
|
+
text-align: center;
|
|
1292
1423
|
}
|
|
1293
|
-
.
|
|
1294
|
-
.
|
|
1295
|
-
.
|
|
1296
|
-
.
|
|
1297
|
-
body.featured-background .pad-grid { width: 49%; height: 72%; }
|
|
1298
|
-
body.featured-background .equalizer { height: 52%; }
|
|
1424
|
+
.feature-grid span:nth-child(3n+1) { border-top: 7px solid #35d4eb; }
|
|
1425
|
+
.feature-grid span:nth-child(3n+2) { border-top: 7px solid #ff9f43; }
|
|
1426
|
+
.feature-grid span:nth-child(3n) { border-top: 7px solid #c9ff41; }
|
|
1427
|
+
body.featured-background .feature-grid { left: 5%; right: 5%; gap: 18px; }
|
|
1299
1428
|
</style>
|
|
1300
1429
|
</head>
|
|
1301
1430
|
<body class="${asset.type}">
|
|
@@ -1313,6 +1442,7 @@ async function createAmazonAssets(client) {
|
|
|
1313
1442
|
|
|
1314
1443
|
const config = JSON.parse(fs.readFileSync(capacitorConfigPath, 'utf8'));
|
|
1315
1444
|
const appName = config.appName || 'App';
|
|
1445
|
+
const storeCopy = getStoreAssetCopy(config);
|
|
1316
1446
|
const iconBuffer = fs.readFileSync(amazonIconPath);
|
|
1317
1447
|
const assets = [
|
|
1318
1448
|
{ fileName: 'icon-1280x720.png', type: 'icon', width: 1280, height: 720 },
|
|
@@ -1338,7 +1468,7 @@ async function createAmazonAssets(client) {
|
|
|
1338
1468
|
const frameTree = await client.send('Page.getFrameTree');
|
|
1339
1469
|
await client.send('Page.setDocumentContent', {
|
|
1340
1470
|
frameId: frameTree.frameTree.frame.id,
|
|
1341
|
-
html: createAmazonAssetHtml(asset, appName, iconBuffer),
|
|
1471
|
+
html: createAmazonAssetHtml(asset, appName, iconBuffer, storeCopy),
|
|
1342
1472
|
});
|
|
1343
1473
|
await client.send('Runtime.evaluate', {
|
|
1344
1474
|
expression: `Promise.all([...document.images].map((image) => image.complete
|
|
@@ -1363,15 +1493,18 @@ async function createAmazonAssets(client) {
|
|
|
1363
1493
|
}
|
|
1364
1494
|
}
|
|
1365
1495
|
|
|
1366
|
-
function createSamsungAssetHtml(asset, appName, iconBuffer) {
|
|
1496
|
+
function createSamsungAssetHtml(asset, appName, iconBuffer, storeCopy) {
|
|
1367
1497
|
const iconDataUrl = `data:image/png;base64,${iconBuffer.toString('base64')}`;
|
|
1368
1498
|
const safeAppName = escapeHtml(appName);
|
|
1369
|
-
const
|
|
1370
|
-
`<button><i>0${index + 1}</i><span>${
|
|
1499
|
+
const featureButtons = storeCopy.featureLabels.map((label, index) => (
|
|
1500
|
+
`<button><i>0${index + 1}</i><span>${escapeHtml(label.toUpperCase())}</span></button>`
|
|
1371
1501
|
)).join('');
|
|
1502
|
+
const safeKicker = escapeHtml(storeCopy.samsungKicker.toUpperCase());
|
|
1503
|
+
const safeAction = escapeHtml(storeCopy.samsungAction.toUpperCase());
|
|
1504
|
+
const safeBody = escapeHtml(storeCopy.samsungBody);
|
|
1372
1505
|
const content = asset.type === 'edge-screen'
|
|
1373
|
-
? `<main class="edge-panel"><header><img src="${iconDataUrl}" /><strong>${safeAppName}</strong></header><section>${
|
|
1374
|
-
: `<main class="single-panel"><header><img src="${iconDataUrl}" /><div><small
|
|
1506
|
+
? `<main class="edge-panel"><header><img src="${iconDataUrl}" /><strong>${safeAppName}</strong></header><section>${featureButtons}</section></main>`
|
|
1507
|
+
: `<main class="single-panel"><header><img src="${iconDataUrl}" /><div><small>${safeKicker}</small><strong>${safeAppName}</strong></div></header><div class="single-action"><span>+</span><strong>${safeAction}</strong><small>${safeBody}</small></div><div class="accent-bars"><i></i><i></i><i></i><i></i><i></i></div></main>`;
|
|
1375
1508
|
|
|
1376
1509
|
return `<!doctype html>
|
|
1377
1510
|
<html>
|
|
@@ -1455,10 +1588,10 @@ function createSamsungAssetHtml(asset, appName, iconBuffer) {
|
|
|
1455
1588
|
}
|
|
1456
1589
|
.single-action strong { margin-top: 65px; font-size: 42px; letter-spacing: 5px; }
|
|
1457
1590
|
.single-action small { margin-top: 18px; color: #a9b7c9; font-size: 24px; }
|
|
1458
|
-
.
|
|
1459
|
-
.
|
|
1460
|
-
.
|
|
1461
|
-
.
|
|
1591
|
+
.accent-bars { display: flex; align-items: center; gap: 18px; height: 300px; margin-top: auto; }
|
|
1592
|
+
.accent-bars i { width: 14px; height: 70px; border-radius: 999px; background: #35d4eb; }
|
|
1593
|
+
.accent-bars i:nth-child(2), .accent-bars i:nth-child(4) { height: 140px; background: #ff5964; }
|
|
1594
|
+
.accent-bars i:nth-child(3) { height: 210px; background: #c9ff41; }
|
|
1462
1595
|
</style>
|
|
1463
1596
|
</head>
|
|
1464
1597
|
<body>${content}</body>
|
|
@@ -1472,6 +1605,7 @@ async function createSamsungAssets(client) {
|
|
|
1472
1605
|
|
|
1473
1606
|
const config = JSON.parse(fs.readFileSync(capacitorConfigPath, 'utf8'));
|
|
1474
1607
|
const appName = config.appName || 'App';
|
|
1608
|
+
const storeCopy = getStoreAssetCopy(config);
|
|
1475
1609
|
const iconBuffer = fs.readFileSync(amazonIconPath);
|
|
1476
1610
|
const assets = [
|
|
1477
1611
|
{ fileName: 'edge-screen-160x2560.png', type: 'edge-screen', width: 160, height: 2560 },
|
|
@@ -1495,7 +1629,7 @@ async function createSamsungAssets(client) {
|
|
|
1495
1629
|
const frameTree = await client.send('Page.getFrameTree');
|
|
1496
1630
|
await client.send('Page.setDocumentContent', {
|
|
1497
1631
|
frameId: frameTree.frameTree.frame.id,
|
|
1498
|
-
html: createSamsungAssetHtml(asset, appName, iconBuffer),
|
|
1632
|
+
html: createSamsungAssetHtml(asset, appName, iconBuffer, storeCopy),
|
|
1499
1633
|
});
|
|
1500
1634
|
await client.send('Runtime.evaluate', {
|
|
1501
1635
|
expression: `Promise.all([...document.images].map((image) => image.complete
|
|
@@ -1528,20 +1662,20 @@ async function createSamsungAssets(client) {
|
|
|
1528
1662
|
}
|
|
1529
1663
|
|
|
1530
1664
|
async function captureFramedScreenshot(
|
|
1531
|
-
client,
|
|
1532
|
-
screenshotBuffer,
|
|
1533
|
-
profile,
|
|
1534
|
-
screenshotFileName,
|
|
1535
|
-
skipExisting = false,
|
|
1665
|
+
client,
|
|
1666
|
+
screenshotBuffer,
|
|
1667
|
+
profile,
|
|
1668
|
+
screenshotFileName,
|
|
1669
|
+
skipExisting = false,
|
|
1536
1670
|
) {
|
|
1537
|
-
const framedProfileDirectory = path.join(inFrameDirectory, profile.outputDirectory);
|
|
1538
|
-
const framedScreenshotPath = path.join(framedProfileDirectory, screenshotFileName);
|
|
1539
|
-
|
|
1540
|
-
if (skipExisting && fs.existsSync(framedScreenshotPath)) {
|
|
1541
|
-
console.log(`Skipped existing framed screenshot: ${framedScreenshotPath}`);
|
|
1542
|
-
return false;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1671
|
+
const framedProfileDirectory = path.join(inFrameDirectory, profile.outputDirectory);
|
|
1672
|
+
const framedScreenshotPath = path.join(framedProfileDirectory, screenshotFileName);
|
|
1673
|
+
|
|
1674
|
+
if (skipExisting && fs.existsSync(framedScreenshotPath)) {
|
|
1675
|
+
console.log(`Skipped existing framed screenshot: ${framedScreenshotPath}`);
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1545
1679
|
const hasCustomFrameSize = Boolean(profile.framedWidth && profile.framedHeight);
|
|
1546
1680
|
const frameWidth = profile.framedWidth || profile.width;
|
|
1547
1681
|
const frameHeight = profile.framedHeight || profile.height;
|
|
@@ -1554,194 +1688,194 @@ async function captureFramedScreenshot(
|
|
|
1554
1688
|
mobile: false,
|
|
1555
1689
|
screenWidth: frameWidth,
|
|
1556
1690
|
screenHeight: frameHeight,
|
|
1557
|
-
positionX: 0,
|
|
1558
|
-
positionY: 0,
|
|
1559
|
-
});
|
|
1560
|
-
|
|
1561
|
-
const frameTree = await client.send('Page.getFrameTree');
|
|
1562
|
-
await client.send('Page.setDocumentContent', {
|
|
1563
|
-
frameId: frameTree.frameTree.frame.id,
|
|
1564
|
-
html: createDeviceFrameHtml(profile, screenshotBuffer),
|
|
1565
|
-
});
|
|
1566
|
-
await client.send('Runtime.evaluate', {
|
|
1567
|
-
expression: `new Promise((resolve) => {
|
|
1568
|
-
const image = document.querySelector('img');
|
|
1569
|
-
const ready = () => requestAnimationFrame(() => requestAnimationFrame(resolve));
|
|
1570
|
-
if (image.complete) ready();
|
|
1571
|
-
else {
|
|
1572
|
-
image.addEventListener('load', ready, { once: true });
|
|
1573
|
-
image.addEventListener('error', ready, { once: true });
|
|
1574
|
-
}
|
|
1575
|
-
})`,
|
|
1576
|
-
awaitPromise: true,
|
|
1577
|
-
returnByValue: true,
|
|
1578
|
-
});
|
|
1579
|
-
|
|
1580
|
-
const framedScreenshot = await client.send('Page.captureScreenshot', {
|
|
1581
|
-
format: 'png',
|
|
1582
|
-
fromSurface: true,
|
|
1583
|
-
captureBeyondViewport: false,
|
|
1584
|
-
});
|
|
1585
|
-
|
|
1586
|
-
fs.mkdirSync(framedProfileDirectory, { recursive: true });
|
|
1587
|
-
fs.writeFileSync(
|
|
1588
|
-
framedScreenshotPath,
|
|
1589
|
-
Buffer.from(framedScreenshot.data, 'base64'),
|
|
1590
|
-
{ flag: 'wx' },
|
|
1591
|
-
);
|
|
1592
|
-
console.log(`Saved framed ${profile.label}: ${framedScreenshotPath}`);
|
|
1593
|
-
return true;
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
async function captureProfile(client, snapshot, profile, screenshotFileName) {
|
|
1597
|
-
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
1598
|
-
width: profile.width,
|
|
1599
|
-
height: profile.height,
|
|
1600
|
-
deviceScaleFactor: profile.deviceScaleFactor,
|
|
1601
|
-
mobile: profile.mobile,
|
|
1602
|
-
screenWidth: profile.width,
|
|
1603
|
-
screenHeight: profile.height,
|
|
1604
|
-
positionX: 0,
|
|
1605
|
-
positionY: 0,
|
|
1606
|
-
});
|
|
1607
|
-
|
|
1608
|
-
const frameTree = await client.send('Page.getFrameTree');
|
|
1609
|
-
await client.send('Page.setDocumentContent', {
|
|
1610
|
-
frameId: frameTree.frameTree.frame.id,
|
|
1611
|
-
html: snapshot.html,
|
|
1612
|
-
});
|
|
1613
|
-
await waitForReplayLayout(client, snapshot);
|
|
1614
|
-
|
|
1615
|
-
const screenshot = await client.send('Page.captureScreenshot', {
|
|
1616
|
-
format: 'png',
|
|
1617
|
-
fromSurface: true,
|
|
1618
|
-
captureBeyondViewport: false,
|
|
1619
|
-
});
|
|
1620
|
-
|
|
1621
|
-
const profileDirectory = path.join(screenshotDirectory, profile.outputDirectory);
|
|
1622
|
-
const screenshotPath = path.join(profileDirectory, screenshotFileName);
|
|
1623
|
-
const screenshotBuffer = Buffer.from(screenshot.data, 'base64');
|
|
1624
|
-
|
|
1625
|
-
fs.mkdirSync(profileDirectory, { recursive: true });
|
|
1626
|
-
fs.writeFileSync(screenshotPath, screenshotBuffer, { flag: 'wx' });
|
|
1627
|
-
console.log(`Saved ${profile.label}: ${screenshotPath}`);
|
|
1628
|
-
await captureFramedScreenshot(
|
|
1629
|
-
client,
|
|
1630
|
-
screenshotBuffer,
|
|
1631
|
-
profile,
|
|
1632
|
-
screenshotFileName,
|
|
1633
|
-
);
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
async function captureScreenshotSet(
|
|
1637
|
-
deviceClient,
|
|
1638
|
-
replayClient,
|
|
1639
|
-
selectedProfiles,
|
|
1640
|
-
fileNameInput,
|
|
1641
|
-
) {
|
|
1642
|
-
console.log('Capturing the current rendered page and resolved native data...');
|
|
1643
|
-
const snapshot = await capturePageSnapshot(deviceClient);
|
|
1644
|
-
const screenshotFileName = getScreenshotFileName(fileNameInput, selectedProfiles);
|
|
1645
|
-
|
|
1646
|
-
for (const profile of selectedProfiles) {
|
|
1647
|
-
await captureProfile(replayClient, snapshot, profile, screenshotFileName);
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
console.log(`Responsive screenshot set completed: ${screenshotFileName}`);
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
async function frameExistingScreenshots(selectedProfiles) {
|
|
1654
|
-
let replayBrowser;
|
|
1655
|
-
let replayClient;
|
|
1656
|
-
let framedCount = 0;
|
|
1657
|
-
let failedCount = 0;
|
|
1658
|
-
|
|
1659
|
-
try {
|
|
1660
|
-
replayBrowser = await launchReplayBrowser();
|
|
1661
|
-
replayClient = await createReplayPage(replayBrowser);
|
|
1662
|
-
|
|
1663
|
-
for (const profile of selectedProfiles) {
|
|
1664
|
-
const profileDirectory = path.join(screenshotDirectory, profile.outputDirectory);
|
|
1665
|
-
|
|
1666
|
-
if (!fs.existsSync(profileDirectory)) {
|
|
1667
|
-
continue;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
const screenshotFiles = fs.readdirSync(profileDirectory, { withFileTypes: true })
|
|
1671
|
-
.filter((entry) => entry.isFile() && /\.png$/i.test(entry.name))
|
|
1672
|
-
.map((entry) => entry.name)
|
|
1673
|
-
.sort((first, second) => first.localeCompare(second, undefined, { numeric: true }));
|
|
1674
|
-
|
|
1675
|
-
for (const screenshotFileName of screenshotFiles) {
|
|
1676
|
-
const screenshotPath = path.join(profileDirectory, screenshotFileName);
|
|
1677
|
-
|
|
1678
|
-
try {
|
|
1679
|
-
const wasCreated = await captureFramedScreenshot(
|
|
1680
|
-
replayClient,
|
|
1681
|
-
fs.readFileSync(screenshotPath),
|
|
1682
|
-
profile,
|
|
1683
|
-
screenshotFileName,
|
|
1684
|
-
true,
|
|
1685
|
-
);
|
|
1686
|
-
if (wasCreated) framedCount += 1;
|
|
1687
|
-
} catch (error) {
|
|
1688
|
-
failedCount += 1;
|
|
1689
|
-
console.error(`Failed to frame ${screenshotPath}: ${error.message}`);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
} finally {
|
|
1694
|
-
if (replayClient) {
|
|
1695
|
-
replayClient.close();
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
if (replayBrowser) {
|
|
1699
|
-
await closeReplayBrowser(replayBrowser);
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
console.log(`Existing screenshot framing completed: ${framedCount} created.`);
|
|
1704
|
-
|
|
1705
|
-
if (failedCount) {
|
|
1706
|
-
throw new Error(`${failedCount} existing screenshot(s) could not be framed.`);
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
async function runInteractiveCaptureLoop(
|
|
1711
|
-
deviceClient,
|
|
1712
|
-
replayClient,
|
|
1713
|
-
selectedProfiles,
|
|
1714
|
-
) {
|
|
1715
|
-
const terminal = readline.createInterface({
|
|
1716
|
-
input: process.stdin,
|
|
1717
|
-
output: process.stdout,
|
|
1718
|
-
prompt: 'Enter a filename, or press Enter for the next number: ',
|
|
1719
|
-
});
|
|
1720
|
-
|
|
1721
|
-
terminal.on('SIGINT', () => terminal.close());
|
|
1722
|
-
|
|
1723
|
-
console.log(`Screenshots will be saved in: ${screenshotDirectory}`);
|
|
1724
|
-
console.log('Press Ctrl+C to exit.');
|
|
1725
|
-
terminal.prompt();
|
|
1726
|
-
|
|
1727
|
-
for await (const fileNameInput of terminal) {
|
|
1728
|
-
try {
|
|
1729
|
-
await captureScreenshotSet(
|
|
1730
|
-
deviceClient,
|
|
1731
|
-
replayClient,
|
|
1732
|
-
selectedProfiles,
|
|
1733
|
-
fileNameInput,
|
|
1734
|
-
);
|
|
1735
|
-
} catch (error) {
|
|
1736
|
-
console.error(`Responsive screenshot failed: ${error.message}`);
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
terminal.prompt();
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
console.log('\nResponsive screenshot tool closed.');
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1691
|
+
positionX: 0,
|
|
1692
|
+
positionY: 0,
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
const frameTree = await client.send('Page.getFrameTree');
|
|
1696
|
+
await client.send('Page.setDocumentContent', {
|
|
1697
|
+
frameId: frameTree.frameTree.frame.id,
|
|
1698
|
+
html: createDeviceFrameHtml(profile, screenshotBuffer),
|
|
1699
|
+
});
|
|
1700
|
+
await client.send('Runtime.evaluate', {
|
|
1701
|
+
expression: `new Promise((resolve) => {
|
|
1702
|
+
const image = document.querySelector('img');
|
|
1703
|
+
const ready = () => requestAnimationFrame(() => requestAnimationFrame(resolve));
|
|
1704
|
+
if (image.complete) ready();
|
|
1705
|
+
else {
|
|
1706
|
+
image.addEventListener('load', ready, { once: true });
|
|
1707
|
+
image.addEventListener('error', ready, { once: true });
|
|
1708
|
+
}
|
|
1709
|
+
})`,
|
|
1710
|
+
awaitPromise: true,
|
|
1711
|
+
returnByValue: true,
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
const framedScreenshot = await client.send('Page.captureScreenshot', {
|
|
1715
|
+
format: 'png',
|
|
1716
|
+
fromSurface: true,
|
|
1717
|
+
captureBeyondViewport: false,
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
fs.mkdirSync(framedProfileDirectory, { recursive: true });
|
|
1721
|
+
fs.writeFileSync(
|
|
1722
|
+
framedScreenshotPath,
|
|
1723
|
+
Buffer.from(framedScreenshot.data, 'base64'),
|
|
1724
|
+
{ flag: 'wx' },
|
|
1725
|
+
);
|
|
1726
|
+
console.log(`Saved framed ${profile.label}: ${framedScreenshotPath}`);
|
|
1727
|
+
return true;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
async function captureProfile(client, snapshot, profile, screenshotFileName) {
|
|
1731
|
+
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
1732
|
+
width: profile.width,
|
|
1733
|
+
height: profile.height,
|
|
1734
|
+
deviceScaleFactor: profile.deviceScaleFactor,
|
|
1735
|
+
mobile: profile.mobile,
|
|
1736
|
+
screenWidth: profile.width,
|
|
1737
|
+
screenHeight: profile.height,
|
|
1738
|
+
positionX: 0,
|
|
1739
|
+
positionY: 0,
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
const frameTree = await client.send('Page.getFrameTree');
|
|
1743
|
+
await client.send('Page.setDocumentContent', {
|
|
1744
|
+
frameId: frameTree.frameTree.frame.id,
|
|
1745
|
+
html: snapshot.html,
|
|
1746
|
+
});
|
|
1747
|
+
await waitForReplayLayout(client, snapshot);
|
|
1748
|
+
|
|
1749
|
+
const screenshot = await client.send('Page.captureScreenshot', {
|
|
1750
|
+
format: 'png',
|
|
1751
|
+
fromSurface: true,
|
|
1752
|
+
captureBeyondViewport: false,
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
const profileDirectory = path.join(screenshotDirectory, profile.outputDirectory);
|
|
1756
|
+
const screenshotPath = path.join(profileDirectory, screenshotFileName);
|
|
1757
|
+
const screenshotBuffer = Buffer.from(screenshot.data, 'base64');
|
|
1758
|
+
|
|
1759
|
+
fs.mkdirSync(profileDirectory, { recursive: true });
|
|
1760
|
+
fs.writeFileSync(screenshotPath, screenshotBuffer, { flag: 'wx' });
|
|
1761
|
+
console.log(`Saved ${profile.label}: ${screenshotPath}`);
|
|
1762
|
+
await captureFramedScreenshot(
|
|
1763
|
+
client,
|
|
1764
|
+
screenshotBuffer,
|
|
1765
|
+
profile,
|
|
1766
|
+
screenshotFileName,
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
async function captureScreenshotSet(
|
|
1771
|
+
deviceClient,
|
|
1772
|
+
replayClient,
|
|
1773
|
+
selectedProfiles,
|
|
1774
|
+
fileNameInput,
|
|
1775
|
+
) {
|
|
1776
|
+
console.log('Capturing the current rendered page and resolved native data...');
|
|
1777
|
+
const snapshot = await capturePageSnapshot(deviceClient);
|
|
1778
|
+
const screenshotFileName = getScreenshotFileName(fileNameInput, selectedProfiles);
|
|
1779
|
+
|
|
1780
|
+
for (const profile of selectedProfiles) {
|
|
1781
|
+
await captureProfile(replayClient, snapshot, profile, screenshotFileName);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
console.log(`Responsive screenshot set completed: ${screenshotFileName}`);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
async function frameExistingScreenshots(selectedProfiles) {
|
|
1788
|
+
let replayBrowser;
|
|
1789
|
+
let replayClient;
|
|
1790
|
+
let framedCount = 0;
|
|
1791
|
+
let failedCount = 0;
|
|
1792
|
+
|
|
1793
|
+
try {
|
|
1794
|
+
replayBrowser = await launchReplayBrowser();
|
|
1795
|
+
replayClient = await createReplayPage(replayBrowser);
|
|
1796
|
+
|
|
1797
|
+
for (const profile of selectedProfiles) {
|
|
1798
|
+
const profileDirectory = path.join(screenshotDirectory, profile.outputDirectory);
|
|
1799
|
+
|
|
1800
|
+
if (!fs.existsSync(profileDirectory)) {
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
const screenshotFiles = fs.readdirSync(profileDirectory, { withFileTypes: true })
|
|
1805
|
+
.filter((entry) => entry.isFile() && /\.png$/i.test(entry.name))
|
|
1806
|
+
.map((entry) => entry.name)
|
|
1807
|
+
.sort((first, second) => first.localeCompare(second, undefined, { numeric: true }));
|
|
1808
|
+
|
|
1809
|
+
for (const screenshotFileName of screenshotFiles) {
|
|
1810
|
+
const screenshotPath = path.join(profileDirectory, screenshotFileName);
|
|
1811
|
+
|
|
1812
|
+
try {
|
|
1813
|
+
const wasCreated = await captureFramedScreenshot(
|
|
1814
|
+
replayClient,
|
|
1815
|
+
fs.readFileSync(screenshotPath),
|
|
1816
|
+
profile,
|
|
1817
|
+
screenshotFileName,
|
|
1818
|
+
true,
|
|
1819
|
+
);
|
|
1820
|
+
if (wasCreated) framedCount += 1;
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
failedCount += 1;
|
|
1823
|
+
console.error(`Failed to frame ${screenshotPath}: ${error.message}`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
} finally {
|
|
1828
|
+
if (replayClient) {
|
|
1829
|
+
replayClient.close();
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
if (replayBrowser) {
|
|
1833
|
+
await closeReplayBrowser(replayBrowser);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
console.log(`Existing screenshot framing completed: ${framedCount} created.`);
|
|
1838
|
+
|
|
1839
|
+
if (failedCount) {
|
|
1840
|
+
throw new Error(`${failedCount} existing screenshot(s) could not be framed.`);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
async function runInteractiveCaptureLoop(
|
|
1845
|
+
deviceClient,
|
|
1846
|
+
replayClient,
|
|
1847
|
+
selectedProfiles,
|
|
1848
|
+
) {
|
|
1849
|
+
const terminal = readline.createInterface({
|
|
1850
|
+
input: process.stdin,
|
|
1851
|
+
output: process.stdout,
|
|
1852
|
+
prompt: 'Enter a filename, or press Enter for the next number: ',
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
terminal.on('SIGINT', () => terminal.close());
|
|
1856
|
+
|
|
1857
|
+
console.log(`Screenshots will be saved in: ${screenshotDirectory}`);
|
|
1858
|
+
console.log('Press Ctrl+C to exit.');
|
|
1859
|
+
terminal.prompt();
|
|
1860
|
+
|
|
1861
|
+
for await (const fileNameInput of terminal) {
|
|
1862
|
+
try {
|
|
1863
|
+
await captureScreenshotSet(
|
|
1864
|
+
deviceClient,
|
|
1865
|
+
replayClient,
|
|
1866
|
+
selectedProfiles,
|
|
1867
|
+
fileNameInput,
|
|
1868
|
+
);
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
console.error(`Responsive screenshot failed: ${error.message}`);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
terminal.prompt();
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
console.log('\nResponsive screenshot tool closed.');
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1745
1879
|
async function getInteractiveOptions(options) {
|
|
1746
1880
|
if (
|
|
1747
1881
|
!process.stdin.isTTY
|
|
@@ -1749,42 +1883,42 @@ async function getInteractiveOptions(options) {
|
|
|
1749
1883
|
|| options.amazonAssets
|
|
1750
1884
|
|| options.samsungAssets
|
|
1751
1885
|
) {
|
|
1752
|
-
return options;
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
const terminal = readline.createInterface({
|
|
1756
|
-
input: process.stdin,
|
|
1757
|
-
output: process.stdout,
|
|
1758
|
-
});
|
|
1759
|
-
|
|
1760
|
-
try {
|
|
1761
|
-
if (!options.profiles) {
|
|
1762
|
-
console.log('Available profiles:');
|
|
1763
|
-
printProfiles();
|
|
1764
|
-
options.profiles = await terminal.question(
|
|
1765
|
-
'Profiles separated by commas, or press Enter for all: ',
|
|
1766
|
-
);
|
|
1767
|
-
}
|
|
1768
|
-
} finally {
|
|
1769
|
-
terminal.close();
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
return options;
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
async function main() {
|
|
1776
|
-
let options = parseArguments();
|
|
1777
|
-
|
|
1778
|
-
if (options.help) {
|
|
1779
|
-
printHelp();
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
if (options.list) {
|
|
1784
|
-
printProfiles();
|
|
1785
|
-
return;
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1886
|
+
return options;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
const terminal = readline.createInterface({
|
|
1890
|
+
input: process.stdin,
|
|
1891
|
+
output: process.stdout,
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
try {
|
|
1895
|
+
if (!options.profiles) {
|
|
1896
|
+
console.log('Available profiles:');
|
|
1897
|
+
printProfiles();
|
|
1898
|
+
options.profiles = await terminal.question(
|
|
1899
|
+
'Profiles separated by commas, or press Enter for all: ',
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
} finally {
|
|
1903
|
+
terminal.close();
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
return options;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
async function main() {
|
|
1910
|
+
let options = parseArguments();
|
|
1911
|
+
|
|
1912
|
+
if (options.help) {
|
|
1913
|
+
printHelp();
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
if (options.list) {
|
|
1918
|
+
printProfiles();
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1788
1922
|
options = await getInteractiveOptions(options);
|
|
1789
1923
|
|
|
1790
1924
|
if (options.amazonAssets) {
|
|
@@ -1822,96 +1956,96 @@ async function main() {
|
|
|
1822
1956
|
}
|
|
1823
1957
|
|
|
1824
1958
|
const selectedProfiles = selectProfiles(options.profiles);
|
|
1825
|
-
|
|
1826
|
-
if (options.frameExisting) {
|
|
1827
|
-
await frameExistingScreenshots(selectedProfiles);
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
const interactiveMode = process.stdin.isTTY && !options.prefix && !options.dryRun;
|
|
1832
|
-
const deviceId = getConnectedDevice();
|
|
1833
|
-
const appId = getAppId();
|
|
1834
|
-
const socketName = getWebViewSocket(deviceId, appId);
|
|
1835
|
-
const localPort = runAdb([
|
|
1836
|
-
'-s',
|
|
1837
|
-
deviceId,
|
|
1838
|
-
'forward',
|
|
1839
|
-
'tcp:0',
|
|
1840
|
-
`localabstract:${socketName}`,
|
|
1841
|
-
]);
|
|
1842
|
-
|
|
1843
|
-
let deviceClient;
|
|
1844
|
-
let replayBrowser;
|
|
1845
|
-
let replayClient;
|
|
1846
|
-
|
|
1847
|
-
try {
|
|
1848
|
-
const target = await getWebViewTarget(localPort);
|
|
1849
|
-
deviceClient = new CdpClient(target.webSocketDebuggerUrl);
|
|
1850
|
-
await deviceClient.connect();
|
|
1851
|
-
await deviceClient.send('Page.enable');
|
|
1852
|
-
await deviceClient.send('Runtime.enable');
|
|
1853
|
-
|
|
1854
|
-
const pageDetails = await getPageDetails(deviceClient);
|
|
1855
|
-
console.log(`Connected device: ${deviceId}`);
|
|
1856
|
-
console.log(`App: ${appId}`);
|
|
1857
|
-
console.log(`Current page: ${pageDetails.title || '(untitled)'}`);
|
|
1858
|
-
console.log(`Current URL: ${pageDetails.url}`);
|
|
1859
|
-
console.log(
|
|
1860
|
-
`Current viewport: ${pageDetails.width}x${pageDetails.height} `
|
|
1861
|
-
+ `at ${pageDetails.devicePixelRatio}x (${pageDetails.readyState})`,
|
|
1862
|
-
);
|
|
1863
|
-
|
|
1864
|
-
if (options.dryRun) {
|
|
1865
|
-
console.log('Dry run completed. No screenshots were created.');
|
|
1866
|
-
return;
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
fs.mkdirSync(screenshotDirectory, { recursive: true });
|
|
1870
|
-
replayBrowser = await launchReplayBrowser();
|
|
1871
|
-
replayClient = await createReplayPage(replayBrowser);
|
|
1872
|
-
|
|
1873
|
-
console.log(
|
|
1874
|
-
'Apple, TV, Chromebook, Mac, and XR profiles are viewport approximations of the live Android WebView.',
|
|
1875
|
-
);
|
|
1876
|
-
|
|
1877
|
-
if (interactiveMode) {
|
|
1878
|
-
await runInteractiveCaptureLoop(
|
|
1879
|
-
deviceClient,
|
|
1880
|
-
replayClient,
|
|
1881
|
-
selectedProfiles,
|
|
1882
|
-
);
|
|
1883
|
-
} else {
|
|
1884
|
-
await captureScreenshotSet(
|
|
1885
|
-
deviceClient,
|
|
1886
|
-
replayClient,
|
|
1887
|
-
selectedProfiles,
|
|
1888
|
-
options.prefix,
|
|
1889
|
-
);
|
|
1890
|
-
}
|
|
1891
|
-
} finally {
|
|
1892
|
-
if (replayClient) {
|
|
1893
|
-
replayClient.close();
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
if (replayBrowser) {
|
|
1897
|
-
await closeReplayBrowser(replayBrowser);
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
if (deviceClient) {
|
|
1901
|
-
deviceClient.close();
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
if (localPort) {
|
|
1905
|
-
try {
|
|
1906
|
-
runAdb(['-s', deviceId, 'forward', '--remove', `tcp:${localPort}`]);
|
|
1907
|
-
} catch {
|
|
1908
|
-
// ADB may already have removed the temporary forwarding rule.
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
main().catch((error) => {
|
|
1915
|
-
console.error(`Responsive screenshot failed: ${error.message}`);
|
|
1916
|
-
process.exitCode = 1;
|
|
1917
|
-
});
|
|
1959
|
+
|
|
1960
|
+
if (options.frameExisting) {
|
|
1961
|
+
await frameExistingScreenshots(selectedProfiles);
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
const interactiveMode = process.stdin.isTTY && !options.prefix && !options.dryRun;
|
|
1966
|
+
const deviceId = getConnectedDevice();
|
|
1967
|
+
const appId = getAppId();
|
|
1968
|
+
const socketName = getWebViewSocket(deviceId, appId);
|
|
1969
|
+
const localPort = runAdb([
|
|
1970
|
+
'-s',
|
|
1971
|
+
deviceId,
|
|
1972
|
+
'forward',
|
|
1973
|
+
'tcp:0',
|
|
1974
|
+
`localabstract:${socketName}`,
|
|
1975
|
+
]);
|
|
1976
|
+
|
|
1977
|
+
let deviceClient;
|
|
1978
|
+
let replayBrowser;
|
|
1979
|
+
let replayClient;
|
|
1980
|
+
|
|
1981
|
+
try {
|
|
1982
|
+
const target = await getWebViewTarget(localPort);
|
|
1983
|
+
deviceClient = new CdpClient(target.webSocketDebuggerUrl);
|
|
1984
|
+
await deviceClient.connect();
|
|
1985
|
+
await deviceClient.send('Page.enable');
|
|
1986
|
+
await deviceClient.send('Runtime.enable');
|
|
1987
|
+
|
|
1988
|
+
const pageDetails = await getPageDetails(deviceClient);
|
|
1989
|
+
console.log(`Connected device: ${deviceId}`);
|
|
1990
|
+
console.log(`App: ${appId}`);
|
|
1991
|
+
console.log(`Current page: ${pageDetails.title || '(untitled)'}`);
|
|
1992
|
+
console.log(`Current URL: ${pageDetails.url}`);
|
|
1993
|
+
console.log(
|
|
1994
|
+
`Current viewport: ${pageDetails.width}x${pageDetails.height} `
|
|
1995
|
+
+ `at ${pageDetails.devicePixelRatio}x (${pageDetails.readyState})`,
|
|
1996
|
+
);
|
|
1997
|
+
|
|
1998
|
+
if (options.dryRun) {
|
|
1999
|
+
console.log('Dry run completed. No screenshots were created.');
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
fs.mkdirSync(screenshotDirectory, { recursive: true });
|
|
2004
|
+
replayBrowser = await launchReplayBrowser();
|
|
2005
|
+
replayClient = await createReplayPage(replayBrowser);
|
|
2006
|
+
|
|
2007
|
+
console.log(
|
|
2008
|
+
'Apple, TV, Chromebook, Mac, and XR profiles are viewport approximations of the live Android WebView.',
|
|
2009
|
+
);
|
|
2010
|
+
|
|
2011
|
+
if (interactiveMode) {
|
|
2012
|
+
await runInteractiveCaptureLoop(
|
|
2013
|
+
deviceClient,
|
|
2014
|
+
replayClient,
|
|
2015
|
+
selectedProfiles,
|
|
2016
|
+
);
|
|
2017
|
+
} else {
|
|
2018
|
+
await captureScreenshotSet(
|
|
2019
|
+
deviceClient,
|
|
2020
|
+
replayClient,
|
|
2021
|
+
selectedProfiles,
|
|
2022
|
+
options.prefix,
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
} finally {
|
|
2026
|
+
if (replayClient) {
|
|
2027
|
+
replayClient.close();
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
if (replayBrowser) {
|
|
2031
|
+
await closeReplayBrowser(replayBrowser);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
if (deviceClient) {
|
|
2035
|
+
deviceClient.close();
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
if (localPort) {
|
|
2039
|
+
try {
|
|
2040
|
+
runAdb(['-s', deviceId, 'forward', '--remove', `tcp:${localPort}`]);
|
|
2041
|
+
} catch {
|
|
2042
|
+
// ADB may already have removed the temporary forwarding rule.
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
main().catch((error) => {
|
|
2049
|
+
console.error(`Responsive screenshot failed: ${error.message}`);
|
|
2050
|
+
process.exitCode = 1;
|
|
2051
|
+
});
|