@playdrop/playdrop-cli 0.11.8 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/build.d.ts +1 -1
  3. package/dist/apps/build.js +4 -6
  4. package/dist/apps/index.d.ts +4 -5
  5. package/dist/apps/index.js +14 -16
  6. package/dist/apps/loadCheck.d.ts +76 -0
  7. package/dist/apps/loadCheck.js +874 -0
  8. package/dist/apps/upload.d.ts +3 -2
  9. package/dist/apps/upload.js +112 -9
  10. package/dist/assetSpecs.js +2 -5
  11. package/dist/catalogue-utils.d.ts +1 -0
  12. package/dist/catalogue-utils.js +1 -0
  13. package/dist/catalogue.d.ts +3 -1
  14. package/dist/catalogue.js +19 -3
  15. package/dist/commands/browse.js +2 -2
  16. package/dist/commands/build.js +4 -1
  17. package/dist/commands/captureListing.d.ts +30 -1
  18. package/dist/commands/captureListing.js +308 -125
  19. package/dist/commands/check.d.ts +8 -0
  20. package/dist/commands/check.js +188 -0
  21. package/dist/commands/create.js +17 -15
  22. package/dist/commands/createRemixContent.js +2 -2
  23. package/dist/commands/detail.js +2 -2
  24. package/dist/commands/dev.js +9 -12
  25. package/dist/commands/devServer.d.ts +2 -2
  26. package/dist/commands/devServer.js +3 -4
  27. package/dist/commands/marketing.js +10 -11
  28. package/dist/commands/play.js +2 -2
  29. package/dist/commands/review.d.ts +20 -0
  30. package/dist/commands/review.js +112 -0
  31. package/dist/commands/upload-content.d.ts +1 -1
  32. package/dist/commands/upload-content.js +1 -1
  33. package/dist/commands/upload.d.ts +2 -1
  34. package/dist/commands/upload.js +9 -52
  35. package/dist/commands/validate.js +4 -161
  36. package/dist/commands/worker/runtime.d.ts +1 -0
  37. package/dist/commands/worker/runtime.js +48 -5
  38. package/dist/commands/worker.d.ts +5 -6
  39. package/dist/commands/worker.js +42 -92
  40. package/dist/config.js +1 -2
  41. package/dist/index.js +33 -49
  42. package/dist/playwright.d.ts +5 -0
  43. package/dist/playwright.js +60 -1
  44. package/node_modules/@playdrop/api-client/dist/client.d.ts +5 -3
  45. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  46. package/node_modules/@playdrop/api-client/dist/client.js +10 -27
  47. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +1 -1
  48. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  49. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +2 -1
  50. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  51. package/node_modules/@playdrop/api-client/dist/domains/apps.js +11 -0
  52. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts +1 -1
  53. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts.map +1 -1
  54. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts +3 -1
  55. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts.map +1 -1
  56. package/node_modules/@playdrop/api-client/dist/domains/auth.js +22 -0
  57. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +1 -1
  58. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
  59. package/node_modules/@playdrop/api-client/dist/index.d.ts +7 -6
  60. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  61. package/node_modules/@playdrop/api-client/dist/index.js +11 -6
  62. package/node_modules/@playdrop/boxel-core/dist/src/entity-cleaner.js +0 -7
  63. package/node_modules/@playdrop/boxel-core/dist/src/entity-cleaner.js.map +1 -1
  64. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/textured-builder.js +2 -46
  65. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/textured-builder.js.map +1 -1
  66. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/voxel-builder.js +2 -22
  67. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/voxel-builder.js.map +1 -1
  68. package/node_modules/@playdrop/boxel-core/dist/src/palette_tools.js.map +1 -1
  69. package/node_modules/@playdrop/boxel-core/dist/src/serialization.js +1 -1
  70. package/node_modules/@playdrop/boxel-core/dist/src/serialization.js.map +1 -1
  71. package/node_modules/@playdrop/boxel-core/dist/src/transforms/textured-boxes/infer-face.js.map +1 -1
  72. package/node_modules/@playdrop/boxel-core/dist/src/transforms/textured-boxes/layer-mode.js +2 -2
  73. package/node_modules/@playdrop/boxel-core/dist/src/transforms/textured-boxes/layer-mode.js.map +1 -1
  74. package/node_modules/@playdrop/boxel-core/dist/src/transforms/textured-boxes/slice.js +0 -11
  75. package/node_modules/@playdrop/boxel-core/dist/src/transforms/textured-boxes/slice.js.map +1 -1
  76. package/node_modules/@playdrop/boxel-core/dist/src/transforms/upscale.js.map +1 -1
  77. package/node_modules/@playdrop/boxel-core/dist/src/validation.d.ts +1 -1
  78. package/node_modules/@playdrop/boxel-core/dist/src/validation.js +0 -20
  79. package/node_modules/@playdrop/boxel-core/dist/src/validation.js.map +1 -1
  80. package/node_modules/@playdrop/boxel-three/dist/src/exporters/glb.d.ts +2 -2
  81. package/node_modules/@playdrop/boxel-three/dist/src/exporters/glb.js +2 -12
  82. package/node_modules/@playdrop/boxel-three/dist/src/voxels/faces.js +1 -125
  83. package/node_modules/@playdrop/config/client-meta.json +2 -2
  84. package/node_modules/@playdrop/config/dist/src/client-meta.d.ts +8 -0
  85. package/node_modules/@playdrop/config/dist/src/client-meta.d.ts.map +1 -0
  86. package/node_modules/@playdrop/config/dist/src/client-meta.js +51 -0
  87. package/node_modules/@playdrop/config/dist/src/deployment.d.ts.map +1 -1
  88. package/node_modules/@playdrop/config/dist/src/deployment.js +2 -32
  89. package/node_modules/@playdrop/config/dist/src/index.d.ts +2 -2
  90. package/node_modules/@playdrop/config/dist/src/index.d.ts.map +1 -1
  91. package/node_modules/@playdrop/config/dist/src/index.js +3 -29
  92. package/node_modules/@playdrop/config/dist/src/runtime-service-origins.d.ts +0 -1
  93. package/node_modules/@playdrop/config/dist/src/runtime-service-origins.d.ts.map +1 -1
  94. package/node_modules/@playdrop/config/dist/src/runtime-service-origins.js +21 -5
  95. package/node_modules/@playdrop/config/dist/src/server/fastify.d.ts +2 -2
  96. package/node_modules/@playdrop/config/dist/src/server/fastify.d.ts.map +1 -1
  97. package/node_modules/@playdrop/config/dist/src/server/fastify.js +0 -1
  98. package/node_modules/@playdrop/config/dist/test/runtime-service-origins.test.js +8 -0
  99. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  100. package/node_modules/@playdrop/types/dist/api.d.ts +25 -24
  101. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  102. package/node_modules/@playdrop/types/dist/api.js +5 -10
  103. package/node_modules/@playdrop/types/dist/version.d.ts +14 -0
  104. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  105. package/package.json +1 -1
  106. package/dist/apps/launchCheck.d.ts +0 -39
  107. package/dist/apps/launchCheck.js +0 -414
  108. package/dist/captureRuntime.d.ts +0 -73
  109. package/dist/captureRuntime.js +0 -1021
  110. package/dist/commands/capture.d.ts +0 -14
  111. package/dist/commands/capture.js +0 -550
  112. package/dist/commands/captureRemote.d.ts +0 -17
  113. package/dist/commands/captureRemote.js +0 -394
  114. package/node_modules/@playdrop/types/dist/entity.d.ts +0 -31
  115. package/node_modules/@playdrop/types/dist/entity.d.ts.map +0 -1
  116. package/node_modules/@playdrop/types/dist/entity.js +0 -67
@@ -1,1021 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.MAX_CAPTURE_TIMEOUT_SECONDS = exports.CAPTURE_LOG_LEVEL_VALUES = void 0;
7
- exports.resolveCaptureLogLevel = resolveCaptureLogLevel;
8
- exports.validateCaptureTimeout = validateCaptureTimeout;
9
- exports.runCapture = runCapture;
10
- const promises_1 = require("node:fs/promises");
11
- const node_path_1 = require("node:path");
12
- const sharp_1 = __importDefault(require("sharp"));
13
- const playwright_1 = require("./playwright");
14
- const sessionCookie_1 = require("./sessionCookie");
15
- const FRAME_SELECTOR = 'iframe[title="Game"]';
16
- exports.CAPTURE_LOG_LEVEL_VALUES = ['debug', 'info', 'warn', 'error'];
17
- exports.MAX_CAPTURE_TIMEOUT_SECONDS = 600;
18
- const GOOGLE_TELEMETRY_HOSTS = [
19
- 'analytics.google.com',
20
- 'googletagmanager.com',
21
- 'google-analytics.com',
22
- 'googleadservices.com',
23
- 'doubleclick.net',
24
- ];
25
- const GOOGLE_COUNTRY_SECOND_LEVEL_DOMAINS = new Set(['ac', 'co', 'com', 'edu', 'gov', 'net', 'org']);
26
- const CANVAS_VISUAL_SAMPLE_SIZE = 64;
27
- const MIN_VISIBLE_CANVAS_EDGE_PX = 48;
28
- const MIN_CANVAS_FOREGROUND_RATIO = 0.018;
29
- const MIN_CANVAS_COLOR_RANGE = 34;
30
- const CANVAS_BACKGROUND_DISTANCE_THRESHOLD = 28;
31
- function isHostOrSubdomain(hostname, domain) {
32
- return hostname === domain || hostname.endsWith(`.${domain}`);
33
- }
34
- function isGoogleSearchAdCollectionUrl(parsed) {
35
- const hostname = parsed.hostname.toLowerCase();
36
- if (hostname !== 'www.google.com') {
37
- const suffix = hostname.startsWith('www.google.') ? hostname.slice('www.google.'.length) : '';
38
- const labels = suffix.split('.');
39
- const isCountryDomain = labels.length === 1
40
- ? /^[a-z]{2}$/.test(labels[0])
41
- : labels.length === 2
42
- && GOOGLE_COUNTRY_SECOND_LEVEL_DOMAINS.has(labels[0])
43
- && /^[a-z]{2}$/.test(labels[1]);
44
- if (!isCountryDomain) {
45
- return false;
46
- }
47
- }
48
- const pathname = parsed.pathname.toLowerCase();
49
- return pathname.startsWith('/ccm/collect')
50
- || pathname.startsWith('/rmkt/collect')
51
- || pathname.startsWith('/pagead/');
52
- }
53
- function isLoopbackOrLocalUrl(rawUrl) {
54
- let parsed;
55
- try {
56
- parsed = new URL(rawUrl);
57
- }
58
- catch {
59
- return false;
60
- }
61
- const hostname = parsed.hostname.toLowerCase();
62
- return hostname === 'localhost'
63
- || hostname === '127.0.0.1'
64
- || hostname === '::1'
65
- || hostname.endsWith('.localhost');
66
- }
67
- function isKnownGoogleTelemetryUrl(rawUrl) {
68
- let parsed;
69
- try {
70
- parsed = new URL(rawUrl);
71
- }
72
- catch {
73
- return false;
74
- }
75
- const hostname = parsed.hostname.toLowerCase();
76
- if (hostname === 'accounts.google.com' || hostname.endsWith('.accounts.google.com')) {
77
- return false;
78
- }
79
- return GOOGLE_TELEMETRY_HOSTS.some(domain => isHostOrSubdomain(hostname, domain))
80
- || isGoogleSearchAdCollectionUrl(parsed);
81
- }
82
- function extractHttpUrls(text) {
83
- const matches = text.match(/https?:\/\/[^\s"'<>]+/gi);
84
- if (!matches) {
85
- return [];
86
- }
87
- return matches.map(match => match.replace(/[\])}.,;]+$/g, ''));
88
- }
89
- function extractRefusedResourceUrls(text) {
90
- const urls = [];
91
- const refusedPattern = /Refused to [^']*'(https?:\/\/[^']+)'/gi;
92
- let match = refusedPattern.exec(text);
93
- while (match) {
94
- urls.push(match[1]);
95
- match = refusedPattern.exec(text);
96
- }
97
- return urls;
98
- }
99
- function isKnownGoogleTelemetryConsoleIssue(text, locationUrl) {
100
- if (/Content Security Policy/i.test(text)) {
101
- const refusedUrls = extractRefusedResourceUrls(text);
102
- if (refusedUrls.some(isKnownGoogleTelemetryUrl)) {
103
- return true;
104
- }
105
- const messageUrls = extractHttpUrls(text);
106
- return messageUrls.some(isKnownGoogleTelemetryUrl)
107
- && !messageUrls.some(isLoopbackOrLocalUrl);
108
- }
109
- if (!/ERR_BLOCKED_BY_CLIENT/i.test(text)) {
110
- return false;
111
- }
112
- const messageUrls = extractHttpUrls(text);
113
- return (locationUrl !== null && isKnownGoogleTelemetryUrl(locationUrl))
114
- || (messageUrls.some(isKnownGoogleTelemetryUrl)
115
- && !messageUrls.some(isLoopbackOrLocalUrl));
116
- }
117
- function formatConsoleValue(value) {
118
- if (typeof value === 'string')
119
- return value;
120
- if (typeof value === 'number' || typeof value === 'boolean' || value === null) {
121
- return String(value);
122
- }
123
- if (typeof value === 'undefined') {
124
- return 'undefined';
125
- }
126
- if (typeof value === 'function') {
127
- return '[function]';
128
- }
129
- if (typeof value === 'object') {
130
- try {
131
- return JSON.stringify(value);
132
- }
133
- catch {
134
- return '[object]';
135
- }
136
- }
137
- return String(value);
138
- }
139
- function serializePayload(payload) {
140
- if (payload === undefined) {
141
- return '';
142
- }
143
- if (typeof payload === 'string') {
144
- return payload;
145
- }
146
- try {
147
- return JSON.stringify(payload);
148
- }
149
- catch {
150
- return String(payload);
151
- }
152
- }
153
- function severityOrder(level) {
154
- switch (level) {
155
- case 'debug':
156
- return 0;
157
- case 'info':
158
- return 1;
159
- case 'warn':
160
- return 2;
161
- case 'error':
162
- return 3;
163
- default:
164
- return 1;
165
- }
166
- }
167
- function mapConsoleTypeToLevel(type) {
168
- const normalized = type.toLowerCase();
169
- if (normalized === 'debug')
170
- return 'debug';
171
- if (normalized === 'warning' || normalized === 'warn')
172
- return 'warn';
173
- if (normalized === 'error' || normalized === 'assert' || normalized === 'trace')
174
- return 'error';
175
- return 'info';
176
- }
177
- function shouldEmit(level, threshold) {
178
- return severityOrder(level) >= severityOrder(threshold);
179
- }
180
- function normalizeComparableUrl(rawUrl) {
181
- const parsed = new URL(rawUrl);
182
- const pathname = parsed.pathname.replace(/\/+$/, '') || '/';
183
- return `${parsed.origin}${pathname}`;
184
- }
185
- function resolveAutomationOrigin(targetUrl) {
186
- const parsed = new URL(targetUrl);
187
- return parsed.searchParams.get('launchCheck') === '1' ? parsed.origin : null;
188
- }
189
- function normalizeHostedLaunchStatePayload(payload) {
190
- if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
191
- return null;
192
- }
193
- const state = typeof payload.state === 'string'
194
- ? payload.state.trim().toLowerCase()
195
- : '';
196
- if (state === 'ready') {
197
- return { state: 'ready' };
198
- }
199
- if (state === 'login_required') {
200
- return { state: 'login_required' };
201
- }
202
- if (state === 'controller_required') {
203
- return { state: 'controller_required' };
204
- }
205
- if (state === 'surface_unsupported') {
206
- return { state: 'surface_unsupported' };
207
- }
208
- if (state !== 'error') {
209
- return null;
210
- }
211
- const errorCode = typeof payload.errorCode === 'string'
212
- ? payload.errorCode.trim() || null
213
- : null;
214
- const message = typeof payload.message === 'string'
215
- ? payload.message.trim() || null
216
- : null;
217
- return {
218
- state: 'error',
219
- errorCode,
220
- message,
221
- };
222
- }
223
- function formatHostedLaunchError(state) {
224
- const parts = ['Hosted app failed to launch.'];
225
- if (state.errorCode) {
226
- parts.push(`errorCode=${state.errorCode}.`);
227
- }
228
- if (state.message) {
229
- parts.push(state.message);
230
- }
231
- return parts.join(' ');
232
- }
233
- function formatHostedLaunchStateMismatch(actualState, expectedState) {
234
- if (expectedState === "login_required") {
235
- return `Hosted app reported ${actualState} instead of login_required.`;
236
- }
237
- if (expectedState === "controller_required") {
238
- return `Hosted app reported ${actualState} instead of controller_required.`;
239
- }
240
- if (expectedState === "surface_unsupported") {
241
- return `Hosted app reported ${actualState} instead of surface_unsupported.`;
242
- }
243
- return `Hosted app reported ${actualState} instead of ready.`;
244
- }
245
- function shouldSettleHostedLaunchWaiter(state, expectedState) {
246
- if (state.state === "error" || !expectedState) {
247
- return true;
248
- }
249
- return state.state === expectedState;
250
- }
251
- function shouldAssertHostedVisualReadiness(input) {
252
- return Boolean(input.requireHostedLaunchReady) && (input.expectedHostedLaunchState ?? 'ready') === 'ready';
253
- }
254
- function summarizeRgbaPixels(pixels, pixelCount, channels) {
255
- let bgR = 0;
256
- let bgG = 0;
257
- let bgB = 0;
258
- const edge = Math.max(1, Math.round(Math.sqrt(pixelCount)));
259
- const cornerIndexes = [
260
- 0,
261
- edge - 1,
262
- edge * (edge - 1),
263
- (edge * edge) - 1,
264
- ].filter(index => index >= 0 && index < pixelCount);
265
- for (const index of cornerIndexes) {
266
- const offset = index * channels;
267
- bgR += pixels[offset] ?? 0;
268
- bgG += pixels[offset + 1] ?? 0;
269
- bgB += pixels[offset + 2] ?? 0;
270
- }
271
- bgR /= cornerIndexes.length || 1;
272
- bgG /= cornerIndexes.length || 1;
273
- bgB /= cornerIndexes.length || 1;
274
- let minLuma = 255;
275
- let maxLuma = 0;
276
- let foreground = 0;
277
- let opaque = 0;
278
- for (let index = 0; index < pixelCount; index += 1) {
279
- const offset = index * channels;
280
- const r = pixels[offset] ?? 0;
281
- const g = pixels[offset + 1] ?? 0;
282
- const b = pixels[offset + 2] ?? 0;
283
- const alpha = channels >= 4 ? pixels[offset + 3] ?? 255 : 255;
284
- if (alpha > 8) {
285
- opaque += 1;
286
- }
287
- const luma = (0.2126 * r) + (0.7152 * g) + (0.0722 * b);
288
- minLuma = Math.min(minLuma, luma);
289
- maxLuma = Math.max(maxLuma, luma);
290
- const distance = Math.hypot(r - bgR, g - bgG, b - bgB);
291
- if (distance >= CANVAS_BACKGROUND_DISTANCE_THRESHOLD && alpha > 8) {
292
- foreground += 1;
293
- }
294
- }
295
- const foregroundRatio = foreground / pixelCount;
296
- const opaqueRatio = opaque / pixelCount;
297
- const colorRange = maxLuma - minLuma;
298
- return {
299
- foregroundRatio,
300
- colorRange,
301
- opaqueRatio,
302
- pass: opaqueRatio > 0.05
303
- && (foregroundRatio >= MIN_CANVAS_FOREGROUND_RATIO || colorRange >= MIN_CANVAS_COLOR_RANGE),
304
- };
305
- }
306
- async function summarizeVisibleCanvases(frame) {
307
- return await frame.evaluate((config) => {
308
- const canvases = Array.from(document.querySelectorAll('canvas'));
309
- const visibleCanvases = canvases.filter((canvas) => {
310
- const rect = canvas.getBoundingClientRect();
311
- const style = window.getComputedStyle(canvas);
312
- const opacity = Number.parseFloat(style.opacity || '1');
313
- return rect.width >= config.minEdge
314
- && rect.height >= config.minEdge
315
- && style.display !== 'none'
316
- && style.visibility !== 'hidden'
317
- && opacity > 0.05;
318
- });
319
- const summaries = visibleCanvases.map((canvas) => {
320
- const rect = canvas.getBoundingClientRect();
321
- const sample = document.createElement('canvas');
322
- sample.width = config.sampleSize;
323
- sample.height = config.sampleSize;
324
- const context = sample.getContext('2d', { willReadFrequently: true });
325
- if (!context) {
326
- return {
327
- x: Math.round(rect.left),
328
- y: Math.round(rect.top),
329
- width: Math.round(rect.width),
330
- height: Math.round(rect.height),
331
- foregroundRatio: 0,
332
- colorRange: 0,
333
- opaqueRatio: 0,
334
- readable: false,
335
- pass: false,
336
- error: 'canvas_2d_context_unavailable',
337
- };
338
- }
339
- try {
340
- context.drawImage(canvas, 0, 0, config.sampleSize, config.sampleSize);
341
- const pixels = context.getImageData(0, 0, config.sampleSize, config.sampleSize).data;
342
- const cornerIndexes = [
343
- 0,
344
- config.sampleSize - 1,
345
- config.sampleSize * (config.sampleSize - 1),
346
- (config.sampleSize * config.sampleSize) - 1,
347
- ];
348
- let bgR = 0;
349
- let bgG = 0;
350
- let bgB = 0;
351
- for (const index of cornerIndexes) {
352
- const offset = index * 4;
353
- bgR += pixels[offset] ?? 0;
354
- bgG += pixels[offset + 1] ?? 0;
355
- bgB += pixels[offset + 2] ?? 0;
356
- }
357
- bgR /= cornerIndexes.length;
358
- bgG /= cornerIndexes.length;
359
- bgB /= cornerIndexes.length;
360
- let minLuma = 255;
361
- let maxLuma = 0;
362
- let foreground = 0;
363
- let opaque = 0;
364
- const total = config.sampleSize * config.sampleSize;
365
- for (let offset = 0; offset < pixels.length; offset += 4) {
366
- const r = pixels[offset] ?? 0;
367
- const g = pixels[offset + 1] ?? 0;
368
- const b = pixels[offset + 2] ?? 0;
369
- const alpha = pixels[offset + 3] ?? 0;
370
- if (alpha > 8) {
371
- opaque += 1;
372
- }
373
- const luma = (0.2126 * r) + (0.7152 * g) + (0.0722 * b);
374
- minLuma = Math.min(minLuma, luma);
375
- maxLuma = Math.max(maxLuma, luma);
376
- const distance = Math.hypot(r - bgR, g - bgG, b - bgB);
377
- if (distance >= config.backgroundDistanceThreshold && alpha > 8) {
378
- foreground += 1;
379
- }
380
- }
381
- const foregroundRatio = foreground / total;
382
- const opaqueRatio = opaque / total;
383
- const colorRange = maxLuma - minLuma;
384
- return {
385
- x: Math.round(rect.left),
386
- y: Math.round(rect.top),
387
- width: Math.round(rect.width),
388
- height: Math.round(rect.height),
389
- foregroundRatio,
390
- colorRange,
391
- opaqueRatio,
392
- readable: true,
393
- pass: opaqueRatio > 0.05
394
- && (foregroundRatio >= config.minForegroundRatio || colorRange >= config.minColorRange),
395
- };
396
- }
397
- catch (error) {
398
- return {
399
- x: Math.round(rect.left),
400
- y: Math.round(rect.top),
401
- width: Math.round(rect.width),
402
- height: Math.round(rect.height),
403
- foregroundRatio: 0,
404
- colorRange: 0,
405
- opaqueRatio: 0,
406
- readable: false,
407
- pass: false,
408
- error: error instanceof Error ? error.message : String(error),
409
- };
410
- }
411
- });
412
- return {
413
- canvasCount: canvases.length,
414
- visibleCanvasCount: visibleCanvases.length,
415
- readableCanvasCount: summaries.filter((summary) => summary.readable).length,
416
- passingCanvasCount: summaries.filter((summary) => summary.pass).length,
417
- canvases: summaries,
418
- };
419
- }, {
420
- sampleSize: CANVAS_VISUAL_SAMPLE_SIZE,
421
- minEdge: MIN_VISIBLE_CANVAS_EDGE_PX,
422
- minForegroundRatio: MIN_CANVAS_FOREGROUND_RATIO,
423
- minColorRange: MIN_CANVAS_COLOR_RANGE,
424
- backgroundDistanceThreshold: CANVAS_BACKGROUND_DISTANCE_THRESHOLD,
425
- });
426
- }
427
- function formatCanvasVisualSummary(summary) {
428
- const readable = summary.canvases.filter(canvas => canvas.readable);
429
- const first = readable[0];
430
- if (!first) {
431
- return `${summary.visibleCanvasCount} visible canvas(es), none readable`;
432
- }
433
- return `${summary.visibleCanvasCount} visible canvas(es), foregroundRatio=${first.foregroundRatio.toFixed(3)}, colorRange=${first.colorRange.toFixed(1)}, opaqueRatio=${first.opaqueRatio.toFixed(3)}`;
434
- }
435
- async function getFramePageOffset(frame) {
436
- try {
437
- const element = await frame.frameElement();
438
- try {
439
- const box = await element.boundingBox();
440
- return box ? { x: box.x, y: box.y } : null;
441
- }
442
- finally {
443
- await element.dispose().catch(() => { });
444
- }
445
- }
446
- catch {
447
- return null;
448
- }
449
- }
450
- function buildCanvasScreenshotClip(frameOffset, canvas, viewport) {
451
- if (!Number.isFinite(canvas.x) || !Number.isFinite(canvas.y)) {
452
- return null;
453
- }
454
- const x = Math.max(0, frameOffset.x + Number(canvas.x));
455
- const y = Math.max(0, frameOffset.y + Number(canvas.y));
456
- const viewportWidth = viewport?.width ?? Number.POSITIVE_INFINITY;
457
- const viewportHeight = viewport?.height ?? Number.POSITIVE_INFINITY;
458
- const width = Math.min(canvas.width, viewportWidth - x);
459
- const height = Math.min(canvas.height, viewportHeight - y);
460
- if (width < MIN_VISIBLE_CANVAS_EDGE_PX || height < MIN_VISIBLE_CANVAS_EDGE_PX) {
461
- return null;
462
- }
463
- return { x, y, width, height };
464
- }
465
- async function summarizeScreenshotClip(page, clip) {
466
- try {
467
- const png = await page.screenshot({ type: 'png', clip });
468
- const { data, info } = await (0, sharp_1.default)(png)
469
- .ensureAlpha()
470
- .resize(CANVAS_VISUAL_SAMPLE_SIZE, CANVAS_VISUAL_SAMPLE_SIZE, { fit: 'fill' })
471
- .raw()
472
- .toBuffer({ resolveWithObject: true });
473
- const pixelSummary = summarizeRgbaPixels(data, info.width * info.height, info.channels);
474
- return {
475
- x: Math.round(clip.x),
476
- y: Math.round(clip.y),
477
- width: Math.round(clip.width),
478
- height: Math.round(clip.height),
479
- foregroundRatio: pixelSummary.foregroundRatio,
480
- colorRange: pixelSummary.colorRange,
481
- opaqueRatio: pixelSummary.opaqueRatio,
482
- readable: true,
483
- pass: pixelSummary.pass,
484
- };
485
- }
486
- catch {
487
- return null;
488
- }
489
- }
490
- async function summarizeCanvasScreenshots(page, frames, summaries) {
491
- const viewport = typeof page.viewportSize === 'function' ? page.viewportSize() : null;
492
- const screenshotSummaries = [];
493
- for (let index = 0; index < frames.length; index += 1) {
494
- const frame = frames[index];
495
- const summary = summaries[index];
496
- if (!frame || !summary || summary.visibleCanvasCount === 0 || summary.passingCanvasCount > 0) {
497
- continue;
498
- }
499
- const frameOffset = await getFramePageOffset(frame);
500
- if (!frameOffset) {
501
- continue;
502
- }
503
- for (const canvas of summary.canvases) {
504
- const clip = buildCanvasScreenshotClip(frameOffset, canvas, viewport);
505
- if (!clip) {
506
- continue;
507
- }
508
- const screenshotSummary = await summarizeScreenshotClip(page, clip);
509
- if (screenshotSummary) {
510
- screenshotSummaries.push(screenshotSummary);
511
- }
512
- }
513
- }
514
- return screenshotSummaries;
515
- }
516
- async function runCaptureActions(page, actions) {
517
- if (!actions?.length) {
518
- return;
519
- }
520
- for (const action of actions) {
521
- if (action.type === 'click') {
522
- await page.mouse.click(action.x, action.y, { button: action.button ?? 'left' });
523
- continue;
524
- }
525
- if (action.type === 'press') {
526
- await page.keyboard.press(action.key);
527
- continue;
528
- }
529
- if (action.type === 'wait') {
530
- await page.waitForTimeout(action.ms);
531
- continue;
532
- }
533
- throw new Error('capture_action_type_unknown');
534
- }
535
- }
536
- async function sampleFrameRate(page, sampleMs) {
537
- if (!Number.isInteger(sampleMs) || sampleMs <= 0) {
538
- throw new Error('capture_fps_sample_invalid');
539
- }
540
- const childFrames = page.frames().filter(frame => frame.parentFrame() !== null);
541
- const target = childFrames[0] ?? page;
542
- const sample = await target.evaluate((durationMs) => new Promise((resolve) => {
543
- const startedAt = performance.now();
544
- let frameCount = 0;
545
- const tick = (timestamp) => {
546
- frameCount += 1;
547
- const elapsedMs = Math.max(timestamp - startedAt, 0);
548
- if (elapsedMs >= durationMs) {
549
- resolve({
550
- fps: frameCount > 0 && elapsedMs > 0 ? frameCount * 1000 / elapsedMs : 0,
551
- sampleMs: elapsedMs,
552
- frameCount,
553
- });
554
- return;
555
- }
556
- requestAnimationFrame(tick);
557
- };
558
- requestAnimationFrame(tick);
559
- }), sampleMs);
560
- return {
561
- fps: Number(sample.fps.toFixed(2)),
562
- sampleMs: Math.round(sample.sampleMs),
563
- frameCount: sample.frameCount,
564
- };
565
- }
566
- async function assertHostedFrameVisualReadiness(page) {
567
- const childFrames = page.frames().filter(frame => frame.parentFrame() !== null);
568
- if (childFrames.length === 0) {
569
- throw new Error('hosted_app_visual_blank: The hosted app reported ready but no game frame was available for visual validation.');
570
- }
571
- const summaries = await Promise.all(childFrames.map(frame => summarizeVisibleCanvases(frame)));
572
- const framesWithCanvases = summaries.filter(summary => summary.visibleCanvasCount > 0);
573
- if (framesWithCanvases.length === 0) {
574
- return;
575
- }
576
- if (framesWithCanvases.some(summary => summary.passingCanvasCount > 0)) {
577
- return;
578
- }
579
- const screenshotSummaries = await summarizeCanvasScreenshots(page, childFrames, summaries);
580
- if (screenshotSummaries.some(summary => summary.pass)) {
581
- return;
582
- }
583
- const readableFrames = framesWithCanvases.filter(summary => summary.readableCanvasCount > 0);
584
- if (readableFrames.length === 0) {
585
- return;
586
- }
587
- const screenshotDetail = screenshotSummaries.length > 0
588
- ? `; screenshot foregroundRatio=${screenshotSummaries[0].foregroundRatio.toFixed(3)}, colorRange=${screenshotSummaries[0].colorRange.toFixed(1)}, opaqueRatio=${screenshotSummaries[0].opaqueRatio.toFixed(3)}`
589
- : '';
590
- const detail = `${readableFrames.map(formatCanvasVisualSummary).join('; ')}${screenshotDetail}`;
591
- throw new Error(`hosted_app_visual_blank: The hosted app reported ready, but its visible canvas looked empty or background-only after startup. ${detail}. Make the playable objects visible in the initial ready state.`);
592
- }
593
- async function writeLogFile(logPath, lines) {
594
- if (!logPath || logPath.trim().length === 0) {
595
- return;
596
- }
597
- await (0, promises_1.mkdir)((0, node_path_1.dirname)(logPath), { recursive: true });
598
- await (0, promises_1.writeFile)(logPath, `${lines.join('\n')}\n`, 'utf8');
599
- console.log(`[capture] Saved logs to ${(0, node_path_1.relative)(process.cwd(), logPath) || logPath}`);
600
- }
601
- function resolveCaptureLogLevel(value) {
602
- if (!value)
603
- return 'info';
604
- const normalized = value.trim().toLowerCase();
605
- if (exports.CAPTURE_LOG_LEVEL_VALUES.includes(normalized)) {
606
- return normalized;
607
- }
608
- throw new Error(`Unsupported log level "${value}". Choose one of: ${exports.CAPTURE_LOG_LEVEL_VALUES.join(', ')}`);
609
- }
610
- function validateCaptureTimeout(value) {
611
- if (value === undefined || Number.isNaN(value)) {
612
- return 5;
613
- }
614
- if (!Number.isFinite(value) || value <= 0) {
615
- throw new Error('Timeout must be a positive number of seconds.');
616
- }
617
- if (value > exports.MAX_CAPTURE_TIMEOUT_SECONDS) {
618
- throw new Error(`Timeout cannot exceed ${exports.MAX_CAPTURE_TIMEOUT_SECONDS} seconds (10 minutes).`);
619
- }
620
- return value;
621
- }
622
- async function runCapture(options) {
623
- const errors = [];
624
- const warnings = [];
625
- const outputLines = [];
626
- const expectedUrl = options.expectedUrl ? normalizeComparableUrl(options.expectedUrl) : null;
627
- const expectedHostedLaunchState = options.requireHostedLaunchReady
628
- ? options.expectedHostedLaunchState ?? 'ready'
629
- : null;
630
- const hasExplicitLogin = Boolean(options.login?.username);
631
- const shouldBootstrapSavedSession = options.savedSessionBootstrap !== false && !hasExplicitLogin;
632
- const bootstrapToken = shouldBootstrapSavedSession ? options.token?.trim() || null : null;
633
- const bootstrapUser = shouldBootstrapSavedSession ? options.user ?? null : null;
634
- let finalUrl = options.targetUrl;
635
- let hostedLaunchState = null;
636
- let frameRate = null;
637
- const record = (level, message) => {
638
- const line = `[capture][${level}] ${message}`;
639
- outputLines.push(line);
640
- if (level === 'error') {
641
- console.error(line);
642
- return;
643
- }
644
- if (level === 'warn') {
645
- console.warn(line);
646
- return;
647
- }
648
- if (level === 'debug') {
649
- console.debug(line);
650
- return;
651
- }
652
- console.log(line);
653
- };
654
- const recordError = (message) => {
655
- errors.push(message);
656
- record('error', message);
657
- };
658
- const recordWarning = (line, lineAlreadyRecorded = false) => {
659
- warnings.push(line);
660
- if (!lineAlreadyRecorded) {
661
- outputLines.push(line);
662
- }
663
- if (shouldEmit('warn', options.minimumLogLevel)) {
664
- console.warn(line);
665
- }
666
- };
667
- try {
668
- await (0, playwright_1.withChromiumPage)(async ({ context, page }) => {
669
- const targetOrigin = new URL(options.targetUrl).origin;
670
- let hostedLaunchWaiterSettled = false;
671
- let resolveHostedLaunchWaiter = null;
672
- const hostedLaunchWaiter = options.requireHostedLaunchReady
673
- ? new Promise((resolve) => {
674
- resolveHostedLaunchWaiter = resolve;
675
- })
676
- : null;
677
- const settleHostedLaunchWaiter = (state) => {
678
- hostedLaunchState = state;
679
- if (!hostedLaunchWaiter || hostedLaunchWaiterSettled) {
680
- return;
681
- }
682
- if (!shouldSettleHostedLaunchWaiter(state, expectedHostedLaunchState)) {
683
- return;
684
- }
685
- hostedLaunchWaiterSettled = true;
686
- resolveHostedLaunchWaiter?.(state);
687
- };
688
- if (bootstrapToken) {
689
- await context.addCookies((0, sessionCookie_1.buildCaptureAccessTokenCookies)(options.targetUrl, bootstrapToken));
690
- }
691
- if (bootstrapToken || bootstrapUser) {
692
- await page.addInitScript(({ token, user }) => {
693
- try {
694
- if (token) {
695
- window.localStorage.setItem('playdrop.accessToken', token);
696
- }
697
- }
698
- catch {
699
- // ignore storage errors
700
- }
701
- try {
702
- if (user) {
703
- window.localStorage.setItem('playdrop.user', JSON.stringify(user));
704
- }
705
- }
706
- catch {
707
- // ignore storage errors
708
- }
709
- try {
710
- window.sessionStorage.removeItem('playdrop.logoutReason');
711
- }
712
- catch {
713
- // ignore storage errors
714
- }
715
- }, { token: bootstrapToken, user: bootstrapUser });
716
- }
717
- if (options.enableCaptureBridge) {
718
- await page.exposeBinding('__playdropCaptureLog', async (_source, type, payload) => {
719
- if (type === 'hosted-launch-state') {
720
- const normalizedHostedState = normalizeHostedLaunchStatePayload(payload);
721
- if (normalizedHostedState) {
722
- settleHostedLaunchWaiter(normalizedHostedState);
723
- }
724
- }
725
- const normalized = typeof type === 'string' ? type.toLowerCase() : 'info';
726
- const level = normalized === 'error' || normalized === 'fatal'
727
- ? 'error'
728
- : normalized === 'warn' || normalized === 'warning'
729
- ? 'warn'
730
- : normalized === 'debug'
731
- ? 'debug'
732
- : 'info';
733
- const serialized = serializePayload(payload);
734
- const line = ['[capture][custom]', normalized, serialized].filter(Boolean).join(' ');
735
- if (level === 'error') {
736
- errors.push(line);
737
- if (shouldEmit(level, options.minimumLogLevel)) {
738
- console.error(line);
739
- }
740
- outputLines.push(line);
741
- return;
742
- }
743
- outputLines.push(line);
744
- if (!shouldEmit(level, options.minimumLogLevel)) {
745
- return;
746
- }
747
- if (level === 'warn') {
748
- console.warn(line);
749
- }
750
- else if (level === 'debug') {
751
- console.debug(line);
752
- }
753
- else {
754
- console.log(line);
755
- }
756
- });
757
- await page.addInitScript(() => {
758
- window.addEventListener('unhandledrejection', event => {
759
- try {
760
- console.error('[capture][unhandledrejection]', event.reason);
761
- }
762
- catch {
763
- console.error('[capture][unhandledrejection]');
764
- }
765
- });
766
- const bridgeWindow = window;
767
- bridgeWindow.playdrop = bridgeWindow.playdrop || {};
768
- bridgeWindow.__playdropCaptureHostedLaunchState = null;
769
- const bridge = (type, payload) => {
770
- if (type === 'hosted-launch-state') {
771
- bridgeWindow.__playdropCaptureHostedLaunchState = payload;
772
- }
773
- try {
774
- const binding = bridgeWindow.__playdropCaptureLog;
775
- if (typeof binding === 'function') {
776
- binding(type, payload);
777
- }
778
- }
779
- catch (error) {
780
- console.error('[capture][bridge-error]', error);
781
- }
782
- };
783
- Object.defineProperty(bridgeWindow.playdrop, 'capture', {
784
- value: bridge,
785
- configurable: true,
786
- writable: true,
787
- });
788
- });
789
- }
790
- const handleConsoleMessage = async (message) => {
791
- const type = message.type();
792
- const level = mapConsoleTypeToLevel(type);
793
- const args = await Promise.all(message.args().map(arg => arg.jsonValue().catch(() => arg.toString())));
794
- const rendered = args.length > 0 ? args.map(formatConsoleValue).join(' ') : message.text();
795
- const line = `[capture][console:${type}] ${rendered}`;
796
- outputLines.push(line);
797
- let effectiveLevel = level;
798
- if (level === 'warn' && line.includes('GL Driver Message')) {
799
- effectiveLevel = 'debug';
800
- }
801
- if (type === 'error' || type === 'assert' || type === 'trace') {
802
- const location = typeof message.location === 'function' ? message.location() : null;
803
- const locationUrl = typeof location?.url === 'string' && location.url.trim().length > 0
804
- ? location.url
805
- : null;
806
- if (isKnownGoogleTelemetryConsoleIssue(rendered, locationUrl)) {
807
- recordWarning(line, true);
808
- return;
809
- }
810
- errors.push(line);
811
- if (shouldEmit(effectiveLevel, options.minimumLogLevel)) {
812
- console.error(line);
813
- }
814
- return;
815
- }
816
- if (!shouldEmit(effectiveLevel, options.minimumLogLevel)) {
817
- return;
818
- }
819
- if (effectiveLevel === 'warn') {
820
- console.warn(line);
821
- }
822
- else if (effectiveLevel === 'debug') {
823
- console.debug(line);
824
- }
825
- else if (effectiveLevel === 'info') {
826
- console.info(line);
827
- }
828
- else {
829
- console.log(line);
830
- }
831
- };
832
- page.on('console', message => {
833
- void handleConsoleMessage(message).catch((error) => {
834
- const text = error instanceof Error ? error.message : String(error);
835
- recordError(`[console-handler] Failed to inspect console message: ${text}`);
836
- });
837
- });
838
- page.on('pageerror', error => {
839
- const text = error?.message ?? String(error);
840
- errors.push(text);
841
- outputLines.push(`[capture][pageerror] ${text}`);
842
- if (shouldEmit('error', options.minimumLogLevel)) {
843
- console.error(`[capture][pageerror] ${text}`);
844
- }
845
- });
846
- page.on('requestfailed', request => {
847
- const failure = request.failure();
848
- const errorText = failure?.errorText ?? '';
849
- const text = `${request.method()} ${request.url()} - ${errorText || 'failed'}`;
850
- if (isKnownGoogleTelemetryUrl(request.url())) {
851
- recordWarning(`[capture][requestfailed] ${text}`);
852
- return;
853
- }
854
- if (/ERR_ABORTED/i.test(errorText) || /ERR_HTTP2_PROTOCOL_ERROR/i.test(errorText)) {
855
- const line = `[capture][requestcancelled] ${text}`;
856
- outputLines.push(line);
857
- if (shouldEmit('debug', options.minimumLogLevel)) {
858
- console.debug(line);
859
- }
860
- return;
861
- }
862
- errors.push(text);
863
- outputLines.push(`[capture][requestfailed] ${text}`);
864
- if (shouldEmit('warn', options.minimumLogLevel)) {
865
- console.error(`[capture][requestfailed] ${text}`);
866
- }
867
- });
868
- page.on('response', response => {
869
- const status = response.status();
870
- if (status < 400) {
871
- return;
872
- }
873
- const text = `${status} ${response.statusText()} ${response.url()}`;
874
- if (status === 404 && /\\?playdrop_channel=/.test(response.url())) {
875
- const line = `[capture][response-reload] ${text}`;
876
- outputLines.push(line);
877
- if (shouldEmit('debug', options.minimumLogLevel)) {
878
- console.debug(line);
879
- }
880
- return;
881
- }
882
- if (isKnownGoogleTelemetryUrl(response.url())) {
883
- recordWarning(`[capture][response] ${text}`);
884
- return;
885
- }
886
- errors.push(text);
887
- outputLines.push(`[capture][response] ${text}`);
888
- if (shouldEmit('warn', options.minimumLogLevel)) {
889
- console.error(`[capture][response] ${text}`);
890
- }
891
- });
892
- if (options.login?.username) {
893
- const loginUrl = options.login.loginUrl?.trim() || `${targetOrigin}/login`;
894
- record('info', `Opening login page ${loginUrl}`);
895
- await page.goto(loginUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
896
- await page.waitForLoadState('networkidle', { timeout: 30000 });
897
- await page.fill('input[name="username"]', options.login.username);
898
- await page.fill('input[name="password"]', options.login.password);
899
- await page.getByRole('button', { name: 'Login' }).click();
900
- await page.waitForURL((url) => {
901
- try {
902
- return !new URL(url.toString()).pathname.startsWith('/login');
903
- }
904
- catch {
905
- return false;
906
- }
907
- }, { timeout: 30000 });
908
- await page.waitForLoadState('networkidle', { timeout: 30000 });
909
- record('info', 'Login succeeded');
910
- }
911
- record('info', `Opening ${options.targetUrl}`);
912
- const response = await page.goto(options.targetUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
913
- if (response && !response.ok()) {
914
- recordError(`[navigation] ${response.status()} ${response.statusText()} ${options.targetUrl}`);
915
- }
916
- const readinessTimeoutMs = Math.min(Math.max(options.timeoutMs, 1000), 30000);
917
- const readinessHandle = await page.waitForFunction(({ frameSelector, waitForHostedLaunchState }) => {
918
- const frame = document.querySelector(frameSelector);
919
- if (frame) {
920
- return 'frame';
921
- }
922
- if (waitForHostedLaunchState) {
923
- const state = window.__playdropCaptureHostedLaunchState;
924
- if (state && typeof state.state === 'string' && state.state.trim().length > 0) {
925
- return `hosted:${state.state.trim().toLowerCase()}`;
926
- }
927
- }
928
- return null;
929
- }, { frameSelector: FRAME_SELECTOR, waitForHostedLaunchState: Boolean(expectedHostedLaunchState) }, { timeout: readinessTimeoutMs });
930
- await readinessHandle.jsonValue();
931
- const assertPageState = async () => {
932
- const currentUrl = page.url();
933
- if (!currentUrl) {
934
- throw new Error('Capture page did not finish loading a URL.');
935
- }
936
- const finalComparableUrl = normalizeComparableUrl(currentUrl);
937
- if (expectedUrl && finalComparableUrl !== expectedUrl) {
938
- throw new Error(`Capture landed on ${finalComparableUrl} instead of ${expectedUrl}.`);
939
- }
940
- const finalPathname = new URL(currentUrl).pathname;
941
- if (finalPathname.startsWith('/login')) {
942
- throw new Error(`Capture landed on the login page (${currentUrl}).`);
943
- }
944
- const frameCount = await page.locator(FRAME_SELECTOR).count();
945
- if ((expectedHostedLaunchState ?? 'ready') === 'ready' && frameCount === 0) {
946
- throw new Error('The Playdrop app frame never appeared.');
947
- }
948
- if (expectedHostedLaunchState === 'login_required' && frameCount > 0) {
949
- throw new Error('The Playdrop auth gate booted the app frame unexpectedly.');
950
- }
951
- if (expectedHostedLaunchState === 'controller_required' && frameCount > 0) {
952
- throw new Error('The Playdrop controller gate booted the app frame unexpectedly.');
953
- }
954
- if (expectedHostedLaunchState === 'surface_unsupported' && frameCount > 0) {
955
- throw new Error('The Playdrop surface gate booted the app frame unexpectedly.');
956
- }
957
- return currentUrl;
958
- };
959
- if (hostedLaunchWaiter) {
960
- const launchState = await Promise.race([
961
- hostedLaunchWaiter,
962
- page.waitForTimeout(options.timeoutMs).then(() => {
963
- if (hostedLaunchState && expectedHostedLaunchState && hostedLaunchState.state !== expectedHostedLaunchState) {
964
- throw new Error(formatHostedLaunchStateMismatch(hostedLaunchState.state, expectedHostedLaunchState));
965
- }
966
- throw new Error(expectedHostedLaunchState === 'login_required'
967
- ? 'Hosted app did not stop at the PlayDrop sign-in gate before the launch-check timeout.'
968
- : expectedHostedLaunchState === 'controller_required'
969
- ? 'Hosted app did not stop at the PlayDrop controller gate before the launch-check timeout.'
970
- : expectedHostedLaunchState === 'surface_unsupported'
971
- ? 'Hosted app did not stop at the PlayDrop unsupported-surface gate before the launch-check timeout.'
972
- : 'Hosted app did not reach the ready state before the launch-check timeout.');
973
- }),
974
- ]);
975
- if (launchState.state === 'error') {
976
- throw new Error(formatHostedLaunchError(launchState));
977
- }
978
- if (expectedHostedLaunchState && launchState.state !== expectedHostedLaunchState) {
979
- throw new Error(formatHostedLaunchStateMismatch(launchState.state, expectedHostedLaunchState));
980
- }
981
- }
982
- finalUrl = await assertPageState();
983
- await runCaptureActions(page, options.actions);
984
- await page.waitForTimeout(options.settleAfterReadyMs ?? options.timeoutMs);
985
- finalUrl = await assertPageState();
986
- if (shouldAssertHostedVisualReadiness({
987
- requireHostedLaunchReady: options.requireHostedLaunchReady,
988
- expectedHostedLaunchState,
989
- })) {
990
- await assertHostedFrameVisualReadiness(page);
991
- }
992
- if (options.screenshotPath) {
993
- const targetDir = (0, node_path_1.dirname)(options.screenshotPath);
994
- if (targetDir && targetDir !== '.' && targetDir !== '/') {
995
- await (0, promises_1.mkdir)(targetDir, { recursive: true });
996
- }
997
- await page.screenshot({ path: options.screenshotPath, fullPage: true });
998
- console.log(`[capture] Saved screenshot to ${(0, node_path_1.relative)(process.cwd(), options.screenshotPath) || options.screenshotPath}`);
999
- }
1000
- if (options.frameRateSampleMs && options.frameRateSampleMs > 0) {
1001
- frameRate = await sampleFrameRate(page, options.frameRateSampleMs);
1002
- record('info', `Frame-rate sample: ${frameRate.fps.toFixed(2)} fps over ${frameRate.sampleMs}ms (${frameRate.frameCount} frames).`);
1003
- }
1004
- }, {
1005
- ...(options.contextOptions ?? {}),
1006
- automationOrigin: resolveAutomationOrigin(options.targetUrl),
1007
- });
1008
- }
1009
- finally {
1010
- await writeLogFile(options.logPath, outputLines);
1011
- }
1012
- return {
1013
- errorCount: errors.length,
1014
- errors: errors.slice(0, 10),
1015
- warningCount: warnings.length,
1016
- warnings,
1017
- finalUrl,
1018
- hostedLaunchState,
1019
- frameRate,
1020
- };
1021
- }