@playdrop/playdrop-cli 0.12.38 → 0.12.39
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/config/client-meta.json +2 -2
- package/dist/apps/build.d.ts +3 -1
- package/dist/apps/build.js +4 -0
- package/dist/apps/loadCheck.d.ts +9 -2
- package/dist/apps/loadCheck.js +167 -32
- package/dist/apps/surfaceProfiles.d.ts +10 -0
- package/dist/apps/surfaceProfiles.js +64 -0
- package/dist/apps/upload.js +2 -0
- package/dist/catalogue.d.ts +7 -1
- package/dist/catalogue.js +27 -0
- package/dist/commands/check.d.ts +1 -0
- package/dist/commands/check.js +68 -2
- package/dist/commands/upload.d.ts +1 -0
- package/dist/commands/upload.js +18 -0
- package/dist/commands/worker/runtime.d.ts +6 -1
- package/dist/commands/worker/runtime.js +20 -3
- package/dist/commands/worker.d.ts +23 -3
- package/dist/commands/worker.js +98 -5
- package/dist/index.js +2 -0
- package/node_modules/@playdrop/config/client-meta.json +2 -2
- package/node_modules/@playdrop/types/dist/api.d.ts +15 -0
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/app-playtest-tape.d.ts +49 -0
- package/node_modules/@playdrop/types/dist/app-playtest-tape.d.ts.map +1 -0
- package/node_modules/@playdrop/types/dist/app-playtest-tape.js +242 -0
- package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
- package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/index.js +2 -0
- package/node_modules/@playdrop/types/dist/instrument-evidence.d.ts +3 -0
- package/node_modules/@playdrop/types/dist/instrument-evidence.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/instrument-evidence.js +56 -5
- package/node_modules/@playdrop/types/dist/version.d.ts +5 -0
- package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
- package/package.json +1 -1
package/config/client-meta.json
CHANGED
package/dist/apps/build.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppSurface, AppType } from '@playdrop/types';
|
|
1
|
+
import type { AppPlaytestTapes, AppSurface, AppType } from '@playdrop/types';
|
|
2
2
|
import type { AppTask } from '../catalogue';
|
|
3
3
|
export type AppBuildMetadata = {
|
|
4
4
|
displayName?: string;
|
|
@@ -7,6 +7,8 @@ export type AppBuildMetadata = {
|
|
|
7
7
|
emoji?: string | null;
|
|
8
8
|
color?: string | null;
|
|
9
9
|
surfaceTargets?: AppSurface[];
|
|
10
|
+
primarySurface?: AppSurface;
|
|
11
|
+
playtestTapes?: AppPlaytestTapes;
|
|
10
12
|
entryPoint?: string;
|
|
11
13
|
};
|
|
12
14
|
export type AppBuildArtifacts = {
|
package/dist/apps/build.js
CHANGED
|
@@ -606,6 +606,10 @@ async function buildApp(task) {
|
|
|
606
606
|
if (task.surfaceTargets && task.surfaceTargets.length > 0) {
|
|
607
607
|
metadata.surfaceTargets = task.surfaceTargets;
|
|
608
608
|
}
|
|
609
|
+
if (task.primarySurface)
|
|
610
|
+
metadata.primarySurface = task.primarySurface;
|
|
611
|
+
if (task.playtestTapes)
|
|
612
|
+
metadata.playtestTapes = task.playtestTapes;
|
|
609
613
|
metadata.entryPoint = bundleArchive.entryPoint;
|
|
610
614
|
return {
|
|
611
615
|
bundleFile,
|
package/dist/apps/loadCheck.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ApiClient } from '@playdrop/api-client';
|
|
2
|
-
import type
|
|
3
|
-
import type { Frame, Page } from 'playwright-core';
|
|
2
|
+
import { type AppPlaytestTape, type AppSurface, type UserResponse } from '@playdrop/types';
|
|
3
|
+
import type { BrowserContextOptions, Frame, Page } from 'playwright-core';
|
|
4
4
|
import type { AppTask } from '../catalogue';
|
|
5
5
|
import { type TaskCaptureSession } from '../appUrls';
|
|
6
|
+
export { PLAYDROP_SURFACE_CONTEXT_OPTIONS } from './surfaceProfiles';
|
|
6
7
|
export type HostedLoadCheckResult = {
|
|
7
8
|
status: 'PASSED' | 'FAILED';
|
|
8
9
|
errorCode: string | null;
|
|
@@ -41,6 +42,8 @@ export type ProjectCheckAction = {
|
|
|
41
42
|
type: 'wait';
|
|
42
43
|
ms: number;
|
|
43
44
|
};
|
|
45
|
+
export declare function resolveLoadCheckSurface(surfaceTargets: unknown, primarySurface?: unknown): AppSurface;
|
|
46
|
+
export declare function cloneLoadCheckContextOptions(surface: AppSurface): BrowserContextOptions;
|
|
44
47
|
export declare function isSoftwareWebGlRenderer(renderer: string | null | undefined): boolean;
|
|
45
48
|
export declare function normalizeProjectCheckActions(value: unknown): ProjectCheckAction[];
|
|
46
49
|
export declare function focusProjectCheckGameFrame(page: Pick<Page, 'mouse'>, frame: Frame): Promise<{
|
|
@@ -49,6 +52,7 @@ export declare function focusProjectCheckGameFrame(page: Pick<Page, 'mouse'>, fr
|
|
|
49
52
|
width: number;
|
|
50
53
|
height: number;
|
|
51
54
|
}>;
|
|
55
|
+
export declare function dispatchPlaytestTapeToFrame(page: Page, frame: Frame, surface: AppSurface, tape: AppPlaytestTape): Promise<void>;
|
|
52
56
|
export declare function dispatchProjectCheckActionsToFrame(page: Page, frame: Frame, actions: readonly ProjectCheckAction[]): Promise<void>;
|
|
53
57
|
export declare function formatHostedLoadCheckFailure(taskName: string, result: HostedLoadCheckResult, scope?: 'local' | 'staged' | 'final'): string;
|
|
54
58
|
export declare function redactHostedLoadCheckSecrets(text: string, sourceUrl: string): string;
|
|
@@ -66,6 +70,9 @@ export declare function runLocalHostedLoadCheck(input: {
|
|
|
66
70
|
allowUnregisteredViewerLaunch?: boolean;
|
|
67
71
|
screenshotPath?: string | null;
|
|
68
72
|
actions?: ProjectCheckAction[];
|
|
73
|
+
surface?: AppSurface;
|
|
74
|
+
playtestTape?: AppPlaytestTape;
|
|
75
|
+
postReadyWaitMs?: number;
|
|
69
76
|
}): Promise<HostedLoadCheckResult>;
|
|
70
77
|
export declare function runUploadedHostedLoadCheck(input: {
|
|
71
78
|
client: ApiClient;
|
package/dist/apps/loadCheck.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PLAYDROP_SURFACE_CONTEXT_OPTIONS = void 0;
|
|
4
|
+
exports.resolveLoadCheckSurface = resolveLoadCheckSurface;
|
|
5
|
+
exports.cloneLoadCheckContextOptions = cloneLoadCheckContextOptions;
|
|
3
6
|
exports.isSoftwareWebGlRenderer = isSoftwareWebGlRenderer;
|
|
4
7
|
exports.normalizeProjectCheckActions = normalizeProjectCheckActions;
|
|
5
8
|
exports.focusProjectCheckGameFrame = focusProjectCheckGameFrame;
|
|
9
|
+
exports.dispatchPlaytestTapeToFrame = dispatchPlaytestTapeToFrame;
|
|
6
10
|
exports.dispatchProjectCheckActionsToFrame = dispatchProjectCheckActionsToFrame;
|
|
7
11
|
exports.formatHostedLoadCheckFailure = formatHostedLoadCheckFailure;
|
|
8
12
|
exports.redactHostedLoadCheckSecrets = redactHostedLoadCheckSecrets;
|
|
9
13
|
exports.runLocalHostedLoadCheck = runLocalHostedLoadCheck;
|
|
10
14
|
exports.runUploadedHostedLoadCheck = runUploadedHostedLoadCheck;
|
|
15
|
+
const types_1 = require("@playdrop/types");
|
|
11
16
|
const promises_1 = require("node:fs/promises");
|
|
12
17
|
const node_path_1 = require("node:path");
|
|
13
18
|
const appUrls_1 = require("../appUrls");
|
|
@@ -19,33 +24,13 @@ const devAuth_1 = require("../devAuth");
|
|
|
19
24
|
const playwright_1 = require("../playwright");
|
|
20
25
|
const sessionCookie_1 = require("../sessionCookie");
|
|
21
26
|
const registration_1 = require("./registration");
|
|
27
|
+
const surfaceProfiles_1 = require("./surfaceProfiles");
|
|
28
|
+
var surfaceProfiles_2 = require("./surfaceProfiles");
|
|
29
|
+
Object.defineProperty(exports, "PLAYDROP_SURFACE_CONTEXT_OPTIONS", { enumerable: true, get: function () { return surfaceProfiles_2.PLAYDROP_SURFACE_CONTEXT_OPTIONS; } });
|
|
22
30
|
const FRAME_SELECTOR = 'iframe[title="Game"]';
|
|
23
31
|
const DEFAULT_HOSTED_LOAD_TIMEOUT_MS = 15000;
|
|
24
32
|
const POST_READY_SETTLE_MS = 750;
|
|
25
|
-
const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1';
|
|
26
33
|
const LOAD_CHECK_SURFACE_ORDER = ['DESKTOP', 'MOBILE_LANDSCAPE', 'MOBILE_PORTRAIT'];
|
|
27
|
-
const LOAD_CHECK_SURFACE_CONTEXT_OPTIONS = {
|
|
28
|
-
DESKTOP: {
|
|
29
|
-
viewport: { width: 1280, height: 720 },
|
|
30
|
-
deviceScaleFactor: 1,
|
|
31
|
-
isMobile: false,
|
|
32
|
-
hasTouch: false,
|
|
33
|
-
},
|
|
34
|
-
MOBILE_LANDSCAPE: {
|
|
35
|
-
viewport: { width: 896, height: 414 },
|
|
36
|
-
deviceScaleFactor: 2,
|
|
37
|
-
isMobile: true,
|
|
38
|
-
hasTouch: true,
|
|
39
|
-
userAgent: MOBILE_USER_AGENT,
|
|
40
|
-
},
|
|
41
|
-
MOBILE_PORTRAIT: {
|
|
42
|
-
viewport: { width: 414, height: 896 },
|
|
43
|
-
deviceScaleFactor: 2,
|
|
44
|
-
isMobile: true,
|
|
45
|
-
hasTouch: true,
|
|
46
|
-
userAgent: MOBILE_USER_AGENT,
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
34
|
const GOOGLE_TELEMETRY_HOSTS = [
|
|
50
35
|
'analytics.google.com',
|
|
51
36
|
'googletagmanager.com',
|
|
@@ -189,8 +174,12 @@ function normalizeLoadCheckSurfaceTargets(value) {
|
|
|
189
174
|
}
|
|
190
175
|
return Array.from(new Set(value.map(normalizeLoadCheckSurface).filter((entry) => Boolean(entry))));
|
|
191
176
|
}
|
|
192
|
-
function resolveLoadCheckSurface(surfaceTargets) {
|
|
177
|
+
function resolveLoadCheckSurface(surfaceTargets, primarySurface) {
|
|
193
178
|
const targets = normalizeLoadCheckSurfaceTargets(surfaceTargets);
|
|
179
|
+
const primary = normalizeLoadCheckSurface(primarySurface);
|
|
180
|
+
if (primary && targets.includes(primary)) {
|
|
181
|
+
return primary;
|
|
182
|
+
}
|
|
194
183
|
for (const surface of LOAD_CHECK_SURFACE_ORDER) {
|
|
195
184
|
if (targets.includes(surface)) {
|
|
196
185
|
return surface;
|
|
@@ -199,11 +188,7 @@ function resolveLoadCheckSurface(surfaceTargets) {
|
|
|
199
188
|
return 'DESKTOP';
|
|
200
189
|
}
|
|
201
190
|
function cloneLoadCheckContextOptions(surface) {
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
...source,
|
|
205
|
-
viewport: source.viewport ? { ...source.viewport } : source.viewport,
|
|
206
|
-
};
|
|
191
|
+
return (0, surfaceProfiles_1.clonePlaydropSurfaceContextOptions)(surface);
|
|
207
192
|
}
|
|
208
193
|
function resolvePostAuthHostedLaunchState(controllerMode) {
|
|
209
194
|
return controllerMode === 'REQUIRED' ? 'controller_required' : 'ready';
|
|
@@ -329,6 +314,131 @@ async function focusProjectCheckGameFrame(page, frame) {
|
|
|
329
314
|
await element.dispose().catch(() => { });
|
|
330
315
|
}
|
|
331
316
|
}
|
|
317
|
+
async function prepareProjectCheckGameFrame(frame) {
|
|
318
|
+
const element = await frame.frameElement();
|
|
319
|
+
try {
|
|
320
|
+
await element.scrollIntoViewIfNeeded().catch(() => { });
|
|
321
|
+
const box = await element.boundingBox();
|
|
322
|
+
if (!box) {
|
|
323
|
+
throw new Error('project_check_game_frame_not_visible');
|
|
324
|
+
}
|
|
325
|
+
await element.focus().catch(() => { });
|
|
326
|
+
await frame.evaluate(() => {
|
|
327
|
+
window.focus();
|
|
328
|
+
if (document.body) {
|
|
329
|
+
document.body.tabIndex = document.body.tabIndex >= 0 ? document.body.tabIndex : -1;
|
|
330
|
+
document.body.focus();
|
|
331
|
+
}
|
|
332
|
+
}).catch(() => { });
|
|
333
|
+
return box;
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
await element.dispose().catch(() => { });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function resolveTapePoint(box, event) {
|
|
340
|
+
return {
|
|
341
|
+
x: box.x + (event.x * box.width),
|
|
342
|
+
y: box.y + (event.y * box.height),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
async function dispatchTouchPoints(session, type, points) {
|
|
346
|
+
await session.send('Input.dispatchTouchEvent', {
|
|
347
|
+
type,
|
|
348
|
+
touchPoints: Array.from(points.entries()).map(([id, point]) => ({
|
|
349
|
+
id,
|
|
350
|
+
x: point.x,
|
|
351
|
+
y: point.y,
|
|
352
|
+
radiusX: 1,
|
|
353
|
+
radiusY: 1,
|
|
354
|
+
force: 1,
|
|
355
|
+
})),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
async function dispatchPlaytestTapeToFrame(page, frame, surface, tape) {
|
|
359
|
+
const box = await prepareProjectCheckGameFrame(frame);
|
|
360
|
+
const mobile = surface !== 'DESKTOP';
|
|
361
|
+
const session = mobile ? await page.context().newCDPSession(page) : null;
|
|
362
|
+
const touchPoints = new Map();
|
|
363
|
+
const startedAt = Date.now();
|
|
364
|
+
try {
|
|
365
|
+
for (const event of tape.events) {
|
|
366
|
+
const waitMs = event.atMs - (Date.now() - startedAt);
|
|
367
|
+
if (waitMs > 0) {
|
|
368
|
+
await page.waitForTimeout(waitMs);
|
|
369
|
+
}
|
|
370
|
+
if (event.type === 'keyDown') {
|
|
371
|
+
await page.keyboard.down(event.key);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (event.type === 'keyUp') {
|
|
375
|
+
await page.keyboard.up(event.key);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (!('x' in event) || !('y' in event)) {
|
|
379
|
+
throw new Error(`project_check_playtest_event_unsupported:${event.type}`);
|
|
380
|
+
}
|
|
381
|
+
const point = resolveTapePoint(box, event);
|
|
382
|
+
const pointerId = 'pointerId' in event ? event.pointerId ?? 0 : 0;
|
|
383
|
+
if (event.type === 'tap') {
|
|
384
|
+
if (mobile && session) {
|
|
385
|
+
touchPoints.set(pointerId, point);
|
|
386
|
+
await dispatchTouchPoints(session, 'touchStart', touchPoints);
|
|
387
|
+
await page.waitForTimeout(event.durationMs ?? types_1.APP_PLAYTEST_DEFAULT_TAP_DURATION_MS);
|
|
388
|
+
touchPoints.delete(pointerId);
|
|
389
|
+
await dispatchTouchPoints(session, 'touchEnd', touchPoints);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
await page.mouse.move(point.x, point.y);
|
|
393
|
+
await page.mouse.down({ button: 'left' });
|
|
394
|
+
await page.waitForTimeout(event.durationMs ?? types_1.APP_PLAYTEST_DEFAULT_TAP_DURATION_MS);
|
|
395
|
+
await page.mouse.up({ button: 'left' });
|
|
396
|
+
}
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (event.type === 'pointerDown') {
|
|
400
|
+
if (mobile && session) {
|
|
401
|
+
touchPoints.set(pointerId, point);
|
|
402
|
+
await dispatchTouchPoints(session, 'touchStart', touchPoints);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
await page.mouse.move(point.x, point.y);
|
|
406
|
+
await page.mouse.down({ button: 'left' });
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (event.type === 'pointerMove') {
|
|
411
|
+
if (mobile && session) {
|
|
412
|
+
touchPoints.set(pointerId, point);
|
|
413
|
+
await dispatchTouchPoints(session, 'touchMove', touchPoints);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
await page.mouse.move(point.x, point.y);
|
|
417
|
+
}
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (event.type === 'pointerUp') {
|
|
421
|
+
if (mobile && session) {
|
|
422
|
+
touchPoints.delete(pointerId);
|
|
423
|
+
await dispatchTouchPoints(session, 'touchEnd', touchPoints);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
await page.mouse.move(point.x, point.y);
|
|
427
|
+
await page.mouse.up({ button: 'left' });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const remainingMs = tape.durationMs - (Date.now() - startedAt);
|
|
432
|
+
if (remainingMs > 0) {
|
|
433
|
+
await page.waitForTimeout(remainingMs);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
if (session) {
|
|
438
|
+
await session.detach().catch(() => { });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
332
442
|
async function dispatchProjectCheckActionsToFrame(page, frame, actions) {
|
|
333
443
|
for (const action of actions) {
|
|
334
444
|
if (action.type === 'wait') {
|
|
@@ -627,10 +737,22 @@ async function runHostedLoadCheck(options) {
|
|
|
627
737
|
if (isSoftwareWebGlRenderer(webglRenderer)) {
|
|
628
738
|
throw new Error(`project_check_webgl_renderer_software: WebGL renderer is "${webglRenderer}". Real hardware acceleration is required.`);
|
|
629
739
|
}
|
|
630
|
-
if (options.actions?.length) {
|
|
740
|
+
if (options.playtestTape && options.actions?.length) {
|
|
741
|
+
throw new Error('project_check_input_mode_conflict: playtest tape and legacy actions cannot run together.');
|
|
742
|
+
}
|
|
743
|
+
if (options.playtestTape) {
|
|
744
|
+
if (!options.playtestSurface) {
|
|
745
|
+
throw new Error('project_check_playtest_surface_required');
|
|
746
|
+
}
|
|
747
|
+
await dispatchPlaytestTapeToFrame(page, frame, options.playtestSurface, options.playtestTape);
|
|
748
|
+
}
|
|
749
|
+
else if (options.actions?.length) {
|
|
631
750
|
await dispatchProjectCheckActionsToFrame(page, frame, options.actions);
|
|
632
751
|
await page.waitForTimeout(POST_READY_SETTLE_MS);
|
|
633
752
|
}
|
|
753
|
+
else if (options.postReadyWaitMs && options.postReadyWaitMs > 0) {
|
|
754
|
+
await page.waitForTimeout(options.postReadyWaitMs);
|
|
755
|
+
}
|
|
634
756
|
screenshotPath = await saveScreenshot(page, options.screenshotPath);
|
|
635
757
|
}, {
|
|
636
758
|
...options.contextOptions,
|
|
@@ -733,7 +855,11 @@ async function runLocalHostedLoadCheck(input) {
|
|
|
733
855
|
? null
|
|
734
856
|
: input.devRouterPort;
|
|
735
857
|
const localAppMetadata = mountedRuntime.runtimeAssetManifest.response.localAppMetadata;
|
|
736
|
-
const
|
|
858
|
+
const declaredSurfaces = normalizeLoadCheckSurfaceTargets(localAppMetadata.surfaceTargets);
|
|
859
|
+
const captureSurface = input.surface ?? resolveLoadCheckSurface(localAppMetadata.surfaceTargets, input.task.primarySurface);
|
|
860
|
+
if (!declaredSurfaces.includes(captureSurface)) {
|
|
861
|
+
return buildFailedLoadCheckResult('surface_unsupported', `Surface ${captureSurface} is not declared in surfaceTargets for "${input.task.name}".`, null);
|
|
862
|
+
}
|
|
737
863
|
const contextOptions = cloneLoadCheckContextOptions(captureSurface);
|
|
738
864
|
const anonymousExpectedState = resolveAnonymousHostedLaunchState({
|
|
739
865
|
...localAppMetadata,
|
|
@@ -754,6 +880,9 @@ async function runLocalHostedLoadCheck(input) {
|
|
|
754
880
|
contextOptions,
|
|
755
881
|
screenshotPath: input.screenshotPath,
|
|
756
882
|
actions: input.actions,
|
|
883
|
+
playtestTape: input.playtestTape,
|
|
884
|
+
playtestSurface: captureSurface,
|
|
885
|
+
postReadyWaitMs: input.postReadyWaitMs,
|
|
757
886
|
});
|
|
758
887
|
}
|
|
759
888
|
if (anonymousExpectedState === 'login_required') {
|
|
@@ -797,6 +926,9 @@ async function runLocalHostedLoadCheck(input) {
|
|
|
797
926
|
contextOptions,
|
|
798
927
|
screenshotPath: input.screenshotPath,
|
|
799
928
|
actions: input.actions,
|
|
929
|
+
playtestTape: input.playtestTape,
|
|
930
|
+
playtestSurface: captureSurface,
|
|
931
|
+
postReadyWaitMs: input.postReadyWaitMs,
|
|
800
932
|
});
|
|
801
933
|
}
|
|
802
934
|
return await runHostedLoadCheck({
|
|
@@ -813,6 +945,9 @@ async function runLocalHostedLoadCheck(input) {
|
|
|
813
945
|
contextOptions,
|
|
814
946
|
screenshotPath: input.screenshotPath,
|
|
815
947
|
actions: input.actions,
|
|
948
|
+
playtestTape: input.playtestTape,
|
|
949
|
+
playtestSurface: captureSurface,
|
|
950
|
+
postReadyWaitMs: input.postReadyWaitMs,
|
|
816
951
|
});
|
|
817
952
|
}
|
|
818
953
|
finally {
|
|
@@ -829,7 +964,7 @@ function appendLoadCheckParams(targetUrl, devAuth) {
|
|
|
829
964
|
}
|
|
830
965
|
async function runUploadedHostedLoadCheck(input) {
|
|
831
966
|
const preview = await input.client.fetchAppUploadLaunchCheckPreview(input.creatorUsername, input.task.name, input.sessionId);
|
|
832
|
-
const captureSurface = resolveLoadCheckSurface(preview.localAppMetadata.surfaceTargets);
|
|
967
|
+
const captureSurface = resolveLoadCheckSurface(preview.localAppMetadata.surfaceTargets, input.task.primarySurface);
|
|
833
968
|
const contextOptions = cloneLoadCheckContextOptions(captureSurface);
|
|
834
969
|
const anonymousExpectedState = resolveAnonymousHostedLaunchState({
|
|
835
970
|
...preview.localAppMetadata,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AppSurface } from '@playdrop/types';
|
|
2
|
+
import type { BrowserContextOptions } from 'playwright-core';
|
|
3
|
+
export type PlaydropSurfaceProfile = {
|
|
4
|
+
contextOptions: BrowserContextOptions;
|
|
5
|
+
playwrightMcpDevice: string | null;
|
|
6
|
+
};
|
|
7
|
+
export declare const PLAYDROP_SURFACE_PROFILES: Record<AppSurface, PlaydropSurfaceProfile>;
|
|
8
|
+
export declare const PLAYDROP_SURFACE_CONTEXT_OPTIONS: Record<AppSurface, BrowserContextOptions>;
|
|
9
|
+
export declare function clonePlaydropSurfaceContextOptions(surface: AppSurface): BrowserContextOptions;
|
|
10
|
+
export declare function buildPlaywrightMcpSurfaceArgs(surface: AppSurface): string[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PLAYDROP_SURFACE_CONTEXT_OPTIONS = exports.PLAYDROP_SURFACE_PROFILES = void 0;
|
|
4
|
+
exports.clonePlaydropSurfaceContextOptions = clonePlaydropSurfaceContextOptions;
|
|
5
|
+
exports.buildPlaywrightMcpSurfaceArgs = buildPlaywrightMcpSurfaceArgs;
|
|
6
|
+
const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1';
|
|
7
|
+
exports.PLAYDROP_SURFACE_PROFILES = {
|
|
8
|
+
DESKTOP: {
|
|
9
|
+
contextOptions: {
|
|
10
|
+
viewport: { width: 1280, height: 720 },
|
|
11
|
+
deviceScaleFactor: 1,
|
|
12
|
+
isMobile: false,
|
|
13
|
+
hasTouch: false,
|
|
14
|
+
},
|
|
15
|
+
playwrightMcpDevice: null,
|
|
16
|
+
},
|
|
17
|
+
MOBILE_LANDSCAPE: {
|
|
18
|
+
contextOptions: {
|
|
19
|
+
viewport: { width: 844, height: 390 },
|
|
20
|
+
screen: { width: 844, height: 390 },
|
|
21
|
+
deviceScaleFactor: 3,
|
|
22
|
+
isMobile: true,
|
|
23
|
+
hasTouch: true,
|
|
24
|
+
userAgent: MOBILE_USER_AGENT,
|
|
25
|
+
},
|
|
26
|
+
playwrightMcpDevice: 'iPhone 13 landscape',
|
|
27
|
+
},
|
|
28
|
+
MOBILE_PORTRAIT: {
|
|
29
|
+
contextOptions: {
|
|
30
|
+
viewport: { width: 390, height: 844 },
|
|
31
|
+
screen: { width: 390, height: 844 },
|
|
32
|
+
deviceScaleFactor: 3,
|
|
33
|
+
isMobile: true,
|
|
34
|
+
hasTouch: true,
|
|
35
|
+
userAgent: MOBILE_USER_AGENT,
|
|
36
|
+
},
|
|
37
|
+
playwrightMcpDevice: 'iPhone 13',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
exports.PLAYDROP_SURFACE_CONTEXT_OPTIONS = {
|
|
41
|
+
DESKTOP: exports.PLAYDROP_SURFACE_PROFILES.DESKTOP.contextOptions,
|
|
42
|
+
MOBILE_LANDSCAPE: exports.PLAYDROP_SURFACE_PROFILES.MOBILE_LANDSCAPE.contextOptions,
|
|
43
|
+
MOBILE_PORTRAIT: exports.PLAYDROP_SURFACE_PROFILES.MOBILE_PORTRAIT.contextOptions,
|
|
44
|
+
};
|
|
45
|
+
function clonePlaydropSurfaceContextOptions(surface) {
|
|
46
|
+
const source = exports.PLAYDROP_SURFACE_PROFILES[surface].contextOptions;
|
|
47
|
+
return {
|
|
48
|
+
...source,
|
|
49
|
+
viewport: source.viewport ? { ...source.viewport } : source.viewport,
|
|
50
|
+
screen: source.screen ? { ...source.screen } : source.screen,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function buildPlaywrightMcpSurfaceArgs(surface) {
|
|
54
|
+
const profile = exports.PLAYDROP_SURFACE_PROFILES[surface];
|
|
55
|
+
const viewport = profile.contextOptions.viewport;
|
|
56
|
+
if (!viewport) {
|
|
57
|
+
throw new Error(`playdrop_surface_viewport_missing:${surface}`);
|
|
58
|
+
}
|
|
59
|
+
return [
|
|
60
|
+
...(profile.playwrightMcpDevice ? ['--device', profile.playwrightMcpDevice] : []),
|
|
61
|
+
'--viewport-size',
|
|
62
|
+
`${viewport.width}x${viewport.height}`,
|
|
63
|
+
];
|
|
64
|
+
}
|
package/dist/apps/upload.js
CHANGED
|
@@ -247,6 +247,8 @@ async function uploadAppVersion(client, task, artifacts, options) {
|
|
|
247
247
|
design: task.design,
|
|
248
248
|
clearTags: options?.clearTags,
|
|
249
249
|
surfaceTargets: task.surfaceTargets,
|
|
250
|
+
primarySurface: task.primarySurface,
|
|
251
|
+
playtestTapes: task.playtestTapes,
|
|
250
252
|
assetSpecSupport: (task.assetSpecSupport ?? []).map((row) => ({
|
|
251
253
|
assetSpecRef: row.assetSpec,
|
|
252
254
|
versionRange: row.versionRange,
|
package/dist/catalogue.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AppAchievementCatalogueDefinition, type AppAuthMode, type AppControllerMode, type AppHostingMode, type AppLeaderboardCatalogueDefinition, type AppMetadataAssetSpecSupport, type AppSurface, type AppType, type AppVersionDesign, type AppVersionVisibility, type AssetSpecContract, type AssetSpecStatus, type AssetSpecValidationKind, type CatalogueTagGroupDefinition, type ContentLicense, type PlayerMetaDefinitionStatus } from '@playdrop/types';
|
|
1
|
+
import { type AppAchievementCatalogueDefinition, type AppAuthMode, type AppControllerMode, type AppHostingMode, type AppLeaderboardCatalogueDefinition, type AppMetadataAssetSpecSupport, type AppPlaytestTapes, type AppSurface, type AppType, type AppVersionDesign, type AppVersionVisibility, type AssetSpecContract, type AssetSpecStatus, type AssetSpecValidationKind, type CatalogueTagGroupDefinition, type ContentLicense, type PlayerMetaDefinitionStatus } from '@playdrop/types';
|
|
2
2
|
import { type AssetSpecSupportDeclaration } from './assetSpecs';
|
|
3
3
|
export type CatalogueJson = {
|
|
4
4
|
apps?: Array<Record<string, unknown>>;
|
|
@@ -40,6 +40,8 @@ export type AppCatalogueEntry = {
|
|
|
40
40
|
remixSource?: string | null;
|
|
41
41
|
remix?: string;
|
|
42
42
|
surfaceTargets?: Record<string, unknown> | AppSurface[] | null;
|
|
43
|
+
primarySurface?: string | null;
|
|
44
|
+
playtestTapes?: unknown;
|
|
43
45
|
design?: unknown;
|
|
44
46
|
version?: string;
|
|
45
47
|
releaseNotes?: string;
|
|
@@ -178,6 +180,8 @@ export type AppTask = {
|
|
|
178
180
|
emoji?: string | null;
|
|
179
181
|
color?: string | null;
|
|
180
182
|
surfaceTargets: AppSurface[];
|
|
183
|
+
primarySurface?: AppSurface;
|
|
184
|
+
playtestTapes?: AppPlaytestTapes;
|
|
181
185
|
design?: AppVersionDesign;
|
|
182
186
|
missingMetadata: string[];
|
|
183
187
|
version?: string;
|
|
@@ -346,6 +350,8 @@ export type ValidatedAppMetadata = {
|
|
|
346
350
|
color?: string | null;
|
|
347
351
|
surfaceTargets: AppSurface[];
|
|
348
352
|
surfaceTargetMap: SurfaceTargetsMap;
|
|
353
|
+
primarySurface?: AppSurface;
|
|
354
|
+
playtestTapes?: AppPlaytestTapes;
|
|
349
355
|
missingFields: string[];
|
|
350
356
|
warnings: string[];
|
|
351
357
|
errors: string[];
|
package/dist/catalogue.js
CHANGED
|
@@ -868,6 +868,29 @@ function validateAppMetadata(entry) {
|
|
|
868
868
|
catch (error) {
|
|
869
869
|
errors.push(error?.message || 'invalid surfaceTargets');
|
|
870
870
|
}
|
|
871
|
+
let primarySurface;
|
|
872
|
+
if (Object.prototype.hasOwnProperty.call(entry, 'primarySurface')) {
|
|
873
|
+
primarySurface = (0, types_1.parseAppSurface)(entry.primarySurface) ?? undefined;
|
|
874
|
+
if (!primarySurface) {
|
|
875
|
+
errors.push('primarySurface must be DESKTOP, MOBILE_LANDSCAPE, or MOBILE_PORTRAIT');
|
|
876
|
+
}
|
|
877
|
+
else if (!surfaceTargetsList.includes(primarySurface)) {
|
|
878
|
+
errors.push(`primarySurface ${primarySurface} must also be enabled in surfaceTargets`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
let playtestTapes;
|
|
882
|
+
if (Object.prototype.hasOwnProperty.call(entry, 'playtestTapes')) {
|
|
883
|
+
const normalized = (0, types_1.normalizeAppPlaytestTapes)(entry.playtestTapes, {
|
|
884
|
+
surfaceTargets: surfaceTargetsList,
|
|
885
|
+
requireComplete: true,
|
|
886
|
+
});
|
|
887
|
+
if (!normalized.ok) {
|
|
888
|
+
errors.push(normalized.error);
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
playtestTapes = normalized.value;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
871
894
|
return {
|
|
872
895
|
displayName,
|
|
873
896
|
description,
|
|
@@ -876,6 +899,8 @@ function validateAppMetadata(entry) {
|
|
|
876
899
|
color,
|
|
877
900
|
surfaceTargets: surfaceTargetsList,
|
|
878
901
|
surfaceTargetMap: surfaceTargetsMap,
|
|
902
|
+
primarySurface,
|
|
903
|
+
playtestTapes,
|
|
879
904
|
missingFields,
|
|
880
905
|
warnings,
|
|
881
906
|
errors,
|
|
@@ -1486,6 +1511,8 @@ function buildAppTasks(rootDir, catalogues, options) {
|
|
|
1486
1511
|
emoji: metadata.emoji === undefined ? undefined : metadata.emoji,
|
|
1487
1512
|
color: metadata.color === undefined ? undefined : metadata.color,
|
|
1488
1513
|
surfaceTargets: metadata.surfaceTargets,
|
|
1514
|
+
primarySurface: metadata.primarySurface,
|
|
1515
|
+
playtestTapes: metadata.playtestTapes,
|
|
1489
1516
|
design,
|
|
1490
1517
|
missingMetadata: metadata.missingFields,
|
|
1491
1518
|
// Versioning fields
|
package/dist/commands/check.d.ts
CHANGED
package/dist/commands/check.js
CHANGED
|
@@ -53,6 +53,13 @@ function readProjectCheckActions(actionsPath) {
|
|
|
53
53
|
function defaultProjectCheckScreenshotPath(appName) {
|
|
54
54
|
return (0, node_path_1.join)('assets', 'marketing', 'playdrop', 'check', `${appName}.png`);
|
|
55
55
|
}
|
|
56
|
+
function buildComparisonScreenshotPath(basePath, run) {
|
|
57
|
+
const extension = (0, node_path_1.extname)(basePath);
|
|
58
|
+
if (!extension) {
|
|
59
|
+
return `${basePath}-${run}.png`;
|
|
60
|
+
}
|
|
61
|
+
return `${basePath.slice(0, -extension.length)}-${run}${extension}`;
|
|
62
|
+
}
|
|
56
63
|
async function check(targetArg, options = {}) {
|
|
57
64
|
let timeoutMs;
|
|
58
65
|
let actions;
|
|
@@ -86,6 +93,27 @@ async function check(targetArg, options = {}) {
|
|
|
86
93
|
process.exitCode = 1;
|
|
87
94
|
return;
|
|
88
95
|
}
|
|
96
|
+
let tapeSurface = null;
|
|
97
|
+
let playtestTape = null;
|
|
98
|
+
if (options.tape) {
|
|
99
|
+
if (actions.length > 0) {
|
|
100
|
+
(0, messages_1.printErrorWithHelp)('--tape and --actions cannot be used together.', ['Use the catalogue playtest tape by itself so the timed sequence remains deterministic.'], { command: 'project check' });
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
tapeSurface = (0, types_1.parseAppSurface)(options.tape);
|
|
105
|
+
if (!tapeSurface) {
|
|
106
|
+
(0, messages_1.printErrorWithHelp)(`Invalid tape surface "${options.tape}".`, ['Use DESKTOP, MOBILE_LANDSCAPE, or MOBILE_PORTRAIT.'], { command: 'project check' });
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
playtestTape = taskLookup.task.playtestTapes?.[tapeSurface] ?? null;
|
|
111
|
+
if (!playtestTape) {
|
|
112
|
+
(0, messages_1.printErrorWithHelp)(`No ${tapeSurface} playtest tape is declared for this app.`, [`Add playtestTapes.${tapeSurface} to catalogue.json.`], { command: 'project check' });
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
89
117
|
let appName = resolvedTarget.appName;
|
|
90
118
|
try {
|
|
91
119
|
const match = (0, catalogue_utils_1.findAppDefinition)(filePath);
|
|
@@ -176,7 +204,7 @@ async function check(targetArg, options = {}) {
|
|
|
176
204
|
console.log(`[check] package.json detected at ${projectLabel}, but no "dev" script was found. Run your app build manually if needed.`);
|
|
177
205
|
}
|
|
178
206
|
}
|
|
179
|
-
const
|
|
207
|
+
const runCheck = async (input) => await (0, loadCheck_1.runLocalHostedLoadCheck)({
|
|
180
208
|
client,
|
|
181
209
|
apiBase: envConfig.apiBase,
|
|
182
210
|
webBase: envConfig.webBase,
|
|
@@ -186,9 +214,47 @@ async function check(targetArg, options = {}) {
|
|
|
186
214
|
token,
|
|
187
215
|
currentUser: null,
|
|
188
216
|
captureSession,
|
|
189
|
-
screenshotPath,
|
|
217
|
+
screenshotPath: input.screenshotPath,
|
|
190
218
|
actions,
|
|
219
|
+
surface: input.surface,
|
|
220
|
+
playtestTape: input.tape,
|
|
221
|
+
postReadyWaitMs: input.postReadyWaitMs,
|
|
191
222
|
});
|
|
223
|
+
if (tapeSurface && playtestTape) {
|
|
224
|
+
const idleScreenshotPath = buildComparisonScreenshotPath(screenshotPath, 'idle');
|
|
225
|
+
const tapeScreenshotPath = buildComparisonScreenshotPath(screenshotPath, 'tape');
|
|
226
|
+
console.log(`[check] Running ${playtestTape.durationMs} ms zero-input baseline on ${tapeSurface}.`);
|
|
227
|
+
const idleResult = await runCheck({
|
|
228
|
+
screenshotPath: idleScreenshotPath,
|
|
229
|
+
surface: tapeSurface,
|
|
230
|
+
postReadyWaitMs: playtestTape.durationMs,
|
|
231
|
+
});
|
|
232
|
+
if (idleResult.status !== 'PASSED') {
|
|
233
|
+
(0, messages_1.printErrorWithHelp)((0, loadCheck_1.formatHostedLoadCheckFailure)(appName, idleResult, 'local'), idleResult.screenshotPath ? [`Idle screenshot: ${idleResult.screenshotPath}`] : [], { command: 'project check' });
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
console.log(`[check] Reopening a clean run and executing the ${tapeSurface} tape.`);
|
|
238
|
+
const tapeResult = await runCheck({
|
|
239
|
+
screenshotPath: tapeScreenshotPath,
|
|
240
|
+
surface: tapeSurface,
|
|
241
|
+
tape: playtestTape,
|
|
242
|
+
});
|
|
243
|
+
if (tapeResult.status !== 'PASSED') {
|
|
244
|
+
(0, messages_1.printErrorWithHelp)((0, loadCheck_1.formatHostedLoadCheckFailure)(appName, tapeResult, 'local'), tapeResult.screenshotPath ? [`Tape screenshot: ${tapeResult.screenshotPath}`] : [], { command: 'project check' });
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
console.log(`[check] Captured matched idle and tape runs for ${currentUsername}/${appName}.`);
|
|
249
|
+
console.log(`[check] Idle: ${idleResult.screenshotPath ?? idleScreenshotPath}`);
|
|
250
|
+
console.log(`[check] Tape: ${tapeResult.screenshotPath ?? tapeScreenshotPath}`);
|
|
251
|
+
console.log('[check] The tape passes only when visible evidence shows it meaningfully beats idle.');
|
|
252
|
+
for (const signal of playtestTape.successSignals) {
|
|
253
|
+
console.log(`[check] Success signal ${signal.kind}: ${signal.description}`);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const result = await runCheck({ screenshotPath });
|
|
192
258
|
if (result.status !== 'PASSED') {
|
|
193
259
|
(0, messages_1.printErrorWithHelp)((0, loadCheck_1.formatHostedLoadCheckFailure)(appName, result, 'local'), result.screenshotPath ? [`Screenshot: ${result.screenshotPath}`] : [], { command: 'project check' });
|
|
194
260
|
process.exitCode = 1;
|
|
@@ -37,6 +37,7 @@ export type WorkerAppPublishResult = {
|
|
|
37
37
|
};
|
|
38
38
|
export type WorkerPlaydropAssetRequirement = 'PACK' | 'ASSET_OR_PACK';
|
|
39
39
|
export declare function resolveWorkerPlaydropAssetRequirement(value: unknown): WorkerPlaydropAssetRequirement | null;
|
|
40
|
+
export declare function assertAgentNewGamePlaytestContract(task: AppTask): void;
|
|
40
41
|
export declare function assertTaskPlaytestEvidenceManifest(task: AppTask, options?: {
|
|
41
42
|
requireOutcomeProof?: boolean;
|
|
42
43
|
}): AgentTaskPlaytestProof | null;
|