@playdrop/playdrop-cli 0.12.38 → 0.12.40

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 (36) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/build.d.ts +3 -1
  3. package/dist/apps/build.js +4 -0
  4. package/dist/apps/loadCheck.d.ts +9 -2
  5. package/dist/apps/loadCheck.js +167 -32
  6. package/dist/apps/surfaceProfiles.d.ts +10 -0
  7. package/dist/apps/surfaceProfiles.js +64 -0
  8. package/dist/apps/upload.js +2 -0
  9. package/dist/catalogue.d.ts +7 -1
  10. package/dist/catalogue.js +27 -0
  11. package/dist/commands/captureListing.d.ts +1 -1
  12. package/dist/commands/captureListing.js +17 -8
  13. package/dist/commands/check.d.ts +1 -0
  14. package/dist/commands/check.js +68 -2
  15. package/dist/commands/upload.d.ts +1 -0
  16. package/dist/commands/upload.js +18 -0
  17. package/dist/commands/worker/runtime.d.ts +6 -1
  18. package/dist/commands/worker/runtime.js +20 -3
  19. package/dist/commands/worker.d.ts +23 -3
  20. package/dist/commands/worker.js +98 -5
  21. package/dist/index.js +2 -0
  22. package/node_modules/@playdrop/config/client-meta.json +2 -2
  23. package/node_modules/@playdrop/types/dist/api.d.ts +15 -0
  24. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  25. package/node_modules/@playdrop/types/dist/app-playtest-tape.d.ts +49 -0
  26. package/node_modules/@playdrop/types/dist/app-playtest-tape.d.ts.map +1 -0
  27. package/node_modules/@playdrop/types/dist/app-playtest-tape.js +242 -0
  28. package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
  29. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  30. package/node_modules/@playdrop/types/dist/index.js +2 -0
  31. package/node_modules/@playdrop/types/dist/instrument-evidence.d.ts +3 -0
  32. package/node_modules/@playdrop/types/dist/instrument-evidence.d.ts.map +1 -1
  33. package/node_modules/@playdrop/types/dist/instrument-evidence.js +56 -5
  34. package/node_modules/@playdrop/types/dist/version.d.ts +5 -0
  35. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  36. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.12.38",
2
+ "version": "0.12.40",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.12.38",
4
+ "runtimeSdkVersion": "0.12.40",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -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 = {
@@ -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,
@@ -1,8 +1,9 @@
1
1
  import type { ApiClient } from '@playdrop/api-client';
2
- import type { UserResponse } from '@playdrop/types';
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;
@@ -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
- const source = LOAD_CHECK_SURFACE_CONTEXT_OPTIONS[surface];
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 captureSurface = resolveLoadCheckSurface(localAppMetadata.surfaceTargets);
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
+ }
@@ -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,
@@ -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
@@ -100,7 +100,7 @@ export declare function resolveListingCaptureBrowserContextOptions(surface: AppS
100
100
  width: number;
101
101
  height: number;
102
102
  }): BrowserContextOptions;
103
- export declare function computeRecordedCrop(measurement: HostedGameMeasurement, rawWidth: number, rawHeight: number): CropRect;
103
+ export declare function computeRecordedCrop(measurement: HostedGameMeasurement, recorder: ListingRecorderMetadata, rawWidth: number, rawHeight: number): CropRect;
104
104
  export declare function runListingRecorder(recorderPath: string, pid: number, durationSeconds: number, rawOutputPath: string, metadataPath: string, audio: boolean, deadlineAt: number): Promise<ListingRecorderMetadata>;
105
105
  export declare function captureListing(targetArg: string | undefined, options?: CaptureListingOptions): Promise<void>;
106
106
  export {};
@@ -487,7 +487,7 @@ async function fitWindowToRequestedGameplay(page, requestedWidth, requestedHeigh
487
487
  function roundCropValue(value) {
488
488
  return Math.round(value);
489
489
  }
490
- function computeRecordedCrop(measurement, rawWidth, rawHeight) {
490
+ function computeRecordedCrop(measurement, recorder, rawWidth, rawHeight) {
491
491
  if (measurement.outerWidth <= 0 || measurement.outerHeight <= 0) {
492
492
  throw new Error('invalid_window_geometry');
493
493
  }
@@ -497,21 +497,30 @@ function computeRecordedCrop(measurement, rawWidth, rawHeight) {
497
497
  if (rawWidth <= 0 || rawHeight <= 0) {
498
498
  throw new Error('invalid_raw_dimensions');
499
499
  }
500
- const horizontalChrome = measurement.outerWidth - measurement.innerWidth;
501
- const verticalChrome = measurement.outerHeight - measurement.innerHeight;
500
+ const nativeOuterWidth = recorder.windowFramePoints.width;
501
+ const nativeOuterHeight = recorder.windowFramePoints.height;
502
+ if (nativeOuterWidth <= 0 || nativeOuterHeight <= 0) {
503
+ throw new Error('invalid_native_window_geometry');
504
+ }
505
+ const horizontalChrome = nativeOuterWidth - measurement.innerWidth;
506
+ const verticalChrome = nativeOuterHeight - measurement.innerHeight;
502
507
  if (horizontalChrome < 0 || verticalChrome < 0) {
503
508
  throw new Error('invalid_chrome_geometry');
504
509
  }
505
- const leftChrome = horizontalChrome / 2;
506
- const bottomChrome = leftChrome;
510
+ const usesSyntheticMobileWindow = Math.abs(measurement.outerWidth - measurement.innerWidth) <= 1
511
+ && Math.abs(measurement.outerHeight - measurement.innerHeight) <= 1
512
+ && (Math.abs(nativeOuterWidth - measurement.outerWidth) > 1
513
+ || Math.abs(nativeOuterHeight - measurement.outerHeight) > 1);
514
+ const leftChrome = usesSyntheticMobileWindow ? 0 : horizontalChrome / 2;
515
+ const bottomChrome = usesSyntheticMobileWindow ? 0 : leftChrome;
507
516
  const topChrome = verticalChrome - bottomChrome;
508
517
  if (topChrome < 0) {
509
518
  throw new Error('invalid_vertical_chrome_geometry');
510
519
  }
511
520
  const cropLeftCss = leftChrome + measurement.iframeRect.left;
512
521
  const cropTopCss = topChrome + measurement.iframeRect.top;
513
- const scaleX = rawWidth / measurement.outerWidth;
514
- const scaleY = rawHeight / measurement.outerHeight;
522
+ const scaleX = rawWidth / nativeOuterWidth;
523
+ const scaleY = rawHeight / nativeOuterHeight;
515
524
  const x = roundCropValue(cropLeftCss * scaleX);
516
525
  const y = roundCropValue(cropTopCss * scaleY);
517
526
  const width = roundCropValue(measurement.iframeRect.width * scaleX);
@@ -1035,7 +1044,7 @@ async function captureListing(targetArg, options = {}) {
1035
1044
  const rawVideoStream = extractVideoStream(rawProbe);
1036
1045
  const rawDurationSeconds = readDurationSeconds(rawProbe);
1037
1046
  const rawAudioTrackCount = countAudioStreams(rawProbe);
1038
- const crop = computeRecordedCrop(measurement, rawVideoStream.width, rawVideoStream.height);
1047
+ const crop = computeRecordedCrop(measurement, recorderMetadata, rawVideoStream.width, rawVideoStream.height);
1039
1048
  console.log(`[listing] Cropping raw capture to gameplay at ${crop.width}x${crop.height}+${crop.x}+${crop.y}.`);
1040
1049
  await encodeListingVideo(surfaceOutputPaths.rawVideoPath, surfaceOutputPaths.finalVideoPath, surfaceOutputPaths.posterPath, crop, dimensions.width, dimensions.height, parsedOptions.fps, parsedOptions.posterAtSeconds, captureDeadlineAt);
1041
1050
  if (exportedAudioPath) {
@@ -3,6 +3,7 @@ type ProjectCheckOptions = {
3
3
  timeout?: string | number;
4
4
  screenshot?: string;
5
5
  actions?: string;
6
+ tape?: string;
6
7
  };
7
8
  export declare function check(targetArg: string | undefined, options?: ProjectCheckOptions): Promise<void>;
8
9
  export {};