@volter/editor-game 0.5.65
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/LICENSE +661 -0
- package/NOTICE +23 -0
- package/contributions/asset-budget-asset.menu.ts +26 -0
- package/contributions/asset-budget.action.ts +18 -0
- package/contributions/asset-budget.document.tsx +21 -0
- package/contributions/asset-budget.menu.ts +22 -0
- package/contributions/audio-unlock.service.ts +17 -0
- package/contributions/audio.utility.tsx +15 -0
- package/contributions/autoplay.service.ts +48 -0
- package/contributions/bridge.command.ts +172 -0
- package/contributions/build-profiles.document.tsx +21 -0
- package/contributions/build-progress.status.tsx +45 -0
- package/contributions/build.action.ts +26 -0
- package/contributions/build.command.ts +27 -0
- package/contributions/build.header.tsx +37 -0
- package/contributions/build.menu.ts +31 -0
- package/contributions/build.service.ts +31 -0
- package/contributions/connection.status.tsx +43 -0
- package/contributions/coverage.service.ts +118 -0
- package/contributions/edit-mode-audio.service.ts +23 -0
- package/contributions/edit-mode-networking.service.ts +30 -0
- package/contributions/game-document.service.ts +25 -0
- package/contributions/game-eval.command.ts +210 -0
- package/contributions/game.layout.ts +19 -0
- package/contributions/gameplay.command.ts +255 -0
- package/contributions/generation.service.ts +96 -0
- package/contributions/generations.status.tsx +53 -0
- package/contributions/ingest.service.ts +80 -0
- package/contributions/instances.command.ts +75 -0
- package/contributions/navmesh.menu.ts +39 -0
- package/contributions/navmesh.service.ts +19 -0
- package/contributions/network.utility.tsx +17 -0
- package/contributions/play.command.ts +355 -0
- package/contributions/profiler.action.ts +16 -0
- package/contributions/profiler.menu.ts +22 -0
- package/contributions/profiler.utility.tsx +17 -0
- package/contributions/react/component-board.service.ts +21 -0
- package/contributions/react/design-time-mount.service.ts +60 -0
- package/contributions/react/pasteboard.action.ts +41 -0
- package/contributions/react/react-inspector.service.ts +85 -0
- package/contributions/react/story-documents.service.ts +44 -0
- package/contributions/scene-document.service.ts +32 -0
- package/contributions/state-watch.action.ts +17 -0
- package/contributions/state-watch.menu.ts +23 -0
- package/contributions/state-watch.utility.tsx +20 -0
- package/contributions/team-playtest.service.ts +124 -0
- package/contributions/three/camera-runtime.inspector.tsx +34 -0
- package/contributions/three/component-board.service.ts +24 -0
- package/contributions/three/component-verbs.command.ts +110 -0
- package/contributions/three/component-verbs.service.ts +92 -0
- package/contributions/three/constraints.inspector.tsx +34 -0
- package/contributions/three/model-asset-sections.service.ts +26 -0
- package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
- package/contributions/three/story-documents.service.ts +31 -0
- package/contributions/three/three-authoring.service.ts +66 -0
- package/contributions/transport.header.tsx +19 -0
- package/contributions/xstate-behavior.action.ts +42 -0
- package/contributions/xstate-behavior.document.tsx +73 -0
- package/contributions/xstate-behavior.inspector.tsx +28 -0
- package/contributions/xstate-behavior.menu.ts +23 -0
- package/package.json +144 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
- package/src/asset-budget/asset-budget-model.ts +799 -0
- package/src/asset-budget/basis-encoder.ts +165 -0
- package/src/asset-budget/gltf-io.ts +154 -0
- package/src/asset-budget/gltf-optimize.ts +384 -0
- package/src/asset-budget/image-dims.ts +78 -0
- package/src/asset-budget/optimize-apply.ts +221 -0
- package/src/audio/AudioDebuggerPanel.tsx +457 -0
- package/src/audio/audio-debugger-model.ts +87 -0
- package/src/bridge/call.ts +60 -0
- package/src/bridge/dispatch.ts +459 -0
- package/src/bridge/live-frames.ts +25 -0
- package/src/bridge/screenshot.ts +316 -0
- package/src/build/BuildProfilesPanel.tsx +476 -0
- package/src/build/build-session.ts +247 -0
- package/src/build/format-bytes.ts +14 -0
- package/src/command-results.ts +36 -0
- package/src/coverage/live-authoring-surface.ts +30 -0
- package/src/coverage/live-project-verbs.ts +162 -0
- package/src/coverage/native-system-coverage.ts +143 -0
- package/src/coverage/root-coverage.ts +79 -0
- package/src/coverage/session-coverage.ts +193 -0
- package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
- package/src/edit-mode/edit-mode-audio.ts +56 -0
- package/src/edit-mode/edit-mode-networking.ts +112 -0
- package/src/game-document/DevicePresetPicker.tsx +84 -0
- package/src/game-document/GameCaptureFrameButton.tsx +45 -0
- package/src/game-document/GameDocument.tsx +215 -0
- package/src/game-document/GamePanel.tsx +662 -0
- package/src/game-document/InstanceInspectorPicker.tsx +183 -0
- package/src/game-document/crowd-debug.ts +183 -0
- package/src/game-document/device-preview.ts +336 -0
- package/src/game-document/game-view-store.ts +107 -0
- package/src/game-document/physics-debug.ts +187 -0
- package/src/generation/GenerationActivity.tsx +421 -0
- package/src/generation/GenerationGallery.css +231 -0
- package/src/generation/generation-documents.tsx +338 -0
- package/src/generation/generation-jobs.ts +128 -0
- package/src/generation/generation-presentation.ts +257 -0
- package/src/host/adapter-reach.ts +445 -0
- package/src/host/adapter-runtime-bindings.ts +303 -0
- package/src/host/after-paint.ts +112 -0
- package/src/host/api/configurations.ts +66 -0
- package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
- package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
- package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
- package/src/host/authoring/contract-scenes-stories.ts +204 -0
- package/src/host/authoring/creation-site-related.ts +55 -0
- package/src/host/authoring/ephemeral-persistence.ts +29 -0
- package/src/host/authoring/gesture-persist.ts +84 -0
- package/src/host/authoring/ingest-data-writer.ts +232 -0
- package/src/host/authoring/ingest-source-persistence.ts +826 -0
- package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
- package/src/host/authoring/mounted-authoring.ts +41 -0
- package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
- package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
- package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
- package/src/host/authoring/pixi-isolation-assets.ts +25 -0
- package/src/host/authoring/pixi-live-write-target.ts +979 -0
- package/src/host/authoring/pixi-source-identity.ts +141 -0
- package/src/host/authoring/pixi-still-presentation.ts +71 -0
- package/src/host/authoring/pixi-structure-history.ts +237 -0
- package/src/host/authoring/pixi-transform-channels.ts +205 -0
- package/src/host/authoring/selection-remount-handoff.ts +23 -0
- package/src/host/authoring/source-persistence-backend.ts +373 -0
- package/src/host/authoring/source-refresh-revisions.ts +81 -0
- package/src/host/authoring/struct-write-pipe.ts +143 -0
- package/src/host/auto-frame-window.ts +89 -0
- package/src/host/binding-resolver.ts +393 -0
- package/src/host/browser-transpile.ts +631 -0
- package/src/host/canvas-entry-runtime.ts +95 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
- package/src/host/components/HeaderTelemetry.tsx +341 -0
- package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
- package/src/host/components/ResolutionPicker.tsx +89 -0
- package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
- package/src/host/components/frame-debugger-model.ts +579 -0
- package/src/host/components/header-telemetry-model.ts +74 -0
- package/src/host/components/scene-document.tsx +447 -0
- package/src/host/components/utility-view-state.ts +87 -0
- package/src/host/components/world-root-stage-binding.tsx +133 -0
- package/src/host/components/world-root-stage.ts +1046 -0
- package/src/host/coverage/authoring-read-probe.ts +583 -0
- package/src/host/coverage/capability-coverage.ts +1587 -0
- package/src/host/coverage/coverage-accounting.ts +261 -0
- package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
- package/src/host/coverage/project-verb-coverage.ts +148 -0
- package/src/host/coverage/system-adapter-coverage.ts +373 -0
- package/src/host/design-system-stories/StoryLayout.tsx +104 -0
- package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
- package/src/host/document-preview-three.ts +110 -0
- package/src/host/entry-adjudication.ts +89 -0
- package/src/host/game-location-guard.ts +150 -0
- package/src/host/game-module-access.ts +196 -0
- package/src/host/game-realm-page.ts +358 -0
- package/src/host/game-realm-reclaim.ts +55 -0
- package/src/host/game-realm-storage.ts +104 -0
- package/src/host/gameplay-export.ts +288 -0
- package/src/host/gameplay-recording.ts +717 -0
- package/src/host/gated-globals.ts +1511 -0
- package/src/host/history/json-history-resource.ts +254 -0
- package/src/host/ingest/registry.ts +261 -0
- package/src/host/instance-extract-actions.ts +120 -0
- package/src/host/instance-fork-actions.ts +120 -0
- package/src/host/play-control-hook.ts +26 -0
- package/src/host/playwright-shim.ts +479 -0
- package/src/host/projection/dom.ts +295 -0
- package/src/host/projection/pixi.ts +288 -0
- package/src/host/r3f-entry-runtime.ts +77 -0
- package/src/host/react-mount-runtime.ts +162 -0
- package/src/host/realm-services.ts +148 -0
- package/src/host/recording-preview.ts +106 -0
- package/src/host/roots/module-root.ts +203 -0
- package/src/host/roots/react-root.ts +181 -0
- package/src/host/same-realm-loop-gate.ts +544 -0
- package/src/host/scene-view-drawability.ts +69 -0
- package/src/host/sdk/tools.ts +31 -0
- package/src/host/served-bundle-runtime-modules.ts +331 -0
- package/src/host/server-log-bridge.ts +60 -0
- package/src/host/staged-projects.ts +24 -0
- package/src/host/stories/mounted-story-viewport-source.ts +64 -0
- package/src/host/stories/story-arg-descriptors.ts +50 -0
- package/src/host/stories/story-media-presence.ts +152 -0
- package/src/host/surface-content.ts +87 -0
- package/src/host/take-named-export.ts +23 -0
- package/src/host/three-ingest-runtime.ts +76 -0
- package/src/host/types-fastnoise-lite.d.ts +7 -0
- package/src/host/types-mikktspace.d.ts +20 -0
- package/src/host/types-troika-three-text.d.ts +7 -0
- package/src/host/use-active-performance-source.ts +53 -0
- package/src/host/viewport-pose-memory.ts +48 -0
- package/src/host/viewport-root-presentation.ts +40 -0
- package/src/ingest/active-ingest.ts +251 -0
- package/src/ingest/active-scene-navigation.ts +43 -0
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
- package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
- package/src/ingest/capture-wait-report.ts +122 -0
- package/src/ingest/deferred-ingest-play.ts +216 -0
- package/src/ingest/deferred-ingest-session.ts +23 -0
- package/src/ingest/discovery-public-ingest.ts +242 -0
- package/src/ingest/dom-stub-mark.ts +7 -0
- package/src/ingest/entry-load.ts +56 -0
- package/src/ingest/game-contract-realm.ts +34 -0
- package/src/ingest/game-pointer-lock.ts +89 -0
- package/src/ingest/held-scene-repaint.ts +73 -0
- package/src/ingest/host-surface-box.ts +174 -0
- package/src/ingest/ingest-boot-viewport.ts +54 -0
- package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
- package/src/ingest/ingest-canvas-scene.ts +50 -0
- package/src/ingest/ingest-evidence-hook.ts +131 -0
- package/src/ingest/ingest-frame-snapshot.ts +183 -0
- package/src/ingest/ingest-play-commands.ts +134 -0
- package/src/ingest/ingest-play-control.ts +299 -0
- package/src/ingest/ingest-render-debug.ts +320 -0
- package/src/ingest/ingest-siblings.ts +487 -0
- package/src/ingest/ingest-status.ts +62 -0
- package/src/ingest/live-ingest-facet.ts +46 -0
- package/src/ingest/module-mode.ts +229 -0
- package/src/ingest/mount-canvas-ingest-root.ts +899 -0
- package/src/ingest/mount-coverage.ts +296 -0
- package/src/ingest/mount-dom-ingest-root.ts +282 -0
- package/src/ingest/mount-ingest-root.ts +744 -0
- package/src/ingest/mount-three-ingest-root.ts +366 -0
- package/src/ingest/resolve-canvas.ts +47 -0
- package/src/ingest/resolve-three.ts +123 -0
- package/src/ingest/served-bundle.ts +109 -0
- package/src/ingest/served-html-boot.ts +292 -0
- package/src/ingest/surface-canvas.ts +84 -0
- package/src/ingest/surface-dom.ts +84 -0
- package/src/ingest/surface-three.ts +128 -0
- package/src/ingest/types.ts +76 -0
- package/src/ingest/unmount-ingest-root.ts +224 -0
- package/src/navmesh/navmesh-actions.ts +27 -0
- package/src/navmesh/navmesh-handler.ts +237 -0
- package/src/navmesh/navmesh-workflow-store.ts +78 -0
- package/src/network/NetworkInspectorPanel.tsx +644 -0
- package/src/network/network-inspector-model.ts +225 -0
- package/src/play/play-boot-stall.ts +118 -0
- package/src/play/play-log-events.ts +26 -0
- package/src/play/play-mode.ts +2553 -0
- package/src/play/play-recording.ts +335 -0
- package/src/play/react-play-live-authoring.ts +165 -0
- package/src/play-bar/PlayBar.tsx +488 -0
- package/src/play-bar/PlayerCountPicker.tsx +100 -0
- package/src/profiler/FrameDebuggerPanel.tsx +458 -0
- package/src/profiler/PerformancePanel.tsx +1068 -0
- package/src/profiler/ProfilerPanel.tsx +53 -0
- package/src/profiler/frame-debugger-store.ts +97 -0
- package/src/profiler/main-thread-busy.ts +90 -0
- package/src/react/design-time-react-mount.ts +782 -0
- package/src/react/dom-authoring-adapter.ts +764 -0
- package/src/react/pasteboard-materialize.ts +154 -0
- package/src/react/react-inspector-section.tsx +2496 -0
- package/src/react/react-world-authoring-adapter.ts +3795 -0
- package/src/react/story-documents/story-args-section.ts +19 -0
- package/src/react/story-documents/story-documents.tsx +1380 -0
- package/src/react/story-paint-bounds.ts +94 -0
- package/src/react/ui-board-document.tsx +101 -0
- package/src/react/ui-board-title.ts +9 -0
- package/src/react/ui-component-board.ts +72 -0
- package/src/services/audio-pose-guard.ts +81 -0
- package/src/services/game-audio-unlock.ts +48 -0
- package/src/state-watch/StateWatchPanel.tsx +535 -0
- package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
- package/src/three/authoring/constraint-inspector-section.tsx +189 -0
- package/src/three/authoring/design-time-renderer.ts +189 -0
- package/src/three/authoring/model-asset-inspector-section.css +41 -0
- package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
- package/src/three/authoring/oid-source-persistence.ts +557 -0
- package/src/three/authoring/r3f-design-session.ts +1174 -0
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
- package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
- package/src/three/authoring/spatial-audio-handles.ts +215 -0
- package/src/three/authoring/spatial-collider-handles.ts +229 -0
- package/src/three/authoring/spatial-joint-handles.ts +114 -0
- package/src/three/authoring/spatial-light-handles.ts +178 -0
- package/src/three/authoring/spatial-lod-handles.ts +93 -0
- package/src/three/authoring/spatial-particle-handles.ts +368 -0
- package/src/three/authoring/three-authoring-adapter.ts +1953 -0
- package/src/three/authoring/three-scene-identity.ts +19 -0
- package/src/three/authoring/three-spatial-handles.ts +161 -0
- package/src/three/authoring/typed-three-inspector.ts +528 -0
- package/src/three/component-verbs/extract-menu.ts +71 -0
- package/src/three/component-verbs/fork-menu.ts +78 -0
- package/src/three/component-verbs/internals-menu.ts +91 -0
- package/src/three/story-documents/three-story-documents.tsx +607 -0
- package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
- package/src/three/three-board/board-framing.ts +412 -0
- package/src/three/three-board/board-layout.ts +401 -0
- package/src/three/three-board/board-scene.ts +901 -0
- package/src/three/three-board/three-component-board.ts +62 -0
- package/src/xstate/XStateBehaviorSection.tsx +130 -0
- package/src/xstate/XStateMachineInspector.tsx +566 -0
- package/src/xstate/character-animation-machine.fixture.ts +74 -0
- package/src/xstate/live-behaviors.ts +106 -0
- package/src/xstate/use-live-actor-state.ts +44 -0
- package/src/xstate/xstate-graph.ts +235 -0
- package/src/xstate/xstate-layout.ts +76 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Fork Component…".
|
|
3
|
+
*
|
|
4
|
+
* The applicability rule and every sentence this action can say, kept pure and
|
|
5
|
+
* adapter-free for the same reason `instance-source-actions.ts` (H3) is — the
|
|
6
|
+
* wiring that knows about adapters lives in
|
|
7
|
+
* `authoring/instance-fork-menu.ts`, and the SOURCE REWRITE lives server-side in
|
|
8
|
+
* `ui-source/plan-fork-component.ts`.
|
|
9
|
+
*
|
|
10
|
+
* THE LABEL IS "Fork Component…", NOT "fork for this instance". H7's own title
|
|
11
|
+
* says "for this instance", and it is worth being precise about what the edit
|
|
12
|
+
* actually reaches: it retargets ONE CALLSITE. When that callsite sits inside a
|
|
13
|
+
* component that is itself rendered many times, every one of those renders now
|
|
14
|
+
* renders the fork. One callsite is the unit of the edit; one callsite is not
|
|
15
|
+
* always one object on screen.
|
|
16
|
+
*
|
|
17
|
+
* The trailing ellipsis follows the platform convention the panel already
|
|
18
|
+
* implies for an action with consequences beyond the row — a fork writes a NEW
|
|
19
|
+
* FILE, which no other hierarchy item does.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { InstanceSourceLocator } from '@volter/editor-core/instance-source-actions';
|
|
23
|
+
|
|
24
|
+
export const FORK_COMPONENT_LABEL = 'Fork Component…';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The adapter surface this action reads. `canForkComponent` is the adapter's
|
|
28
|
+
* own honest answer to "could I write this?" (a write backend exists, this id
|
|
29
|
+
* has both a callsite and a definition); `forkComponent` performs it and
|
|
30
|
+
* returns the sentence to show.
|
|
31
|
+
*/
|
|
32
|
+
export interface InstanceForkSource extends InstanceSourceLocator {
|
|
33
|
+
readonly canForkComponent?: ((id: string) => boolean) | undefined;
|
|
34
|
+
readonly forkComponent?: ((id: string) => Promise<string>) | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* H7 applicability. Three conditions, all of them about whether the edit is
|
|
39
|
+
* REACHABLE — never about whether it would succeed, which only the plan can
|
|
40
|
+
* say:
|
|
41
|
+
*
|
|
42
|
+
* 1. the row is a component instance (`role: 'component'`, H1's own predicate);
|
|
43
|
+
* 2. BOTH source destinations resolve — the callsite is the tag this action
|
|
44
|
+
* retargets and the definition is the module it copies, so a row missing
|
|
45
|
+
* either has nothing to fork;
|
|
46
|
+
* 3. the adapter can actually write (a dev-server write backend is present).
|
|
47
|
+
*
|
|
48
|
+
* Like H3, an inapplicable row gets NO item rather than a disabled one: the
|
|
49
|
+
* registry has no disabled state, and the panel's idiom is to omit an item
|
|
50
|
+
* whose subject is absent. Everything the PLAN rejects is a click-time
|
|
51
|
+
* transient hint carrying the plan's own named reason.
|
|
52
|
+
*/
|
|
53
|
+
export function canForkInstance(
|
|
54
|
+
node: { readonly role?: string | undefined } | null | undefined,
|
|
55
|
+
source: InstanceForkSource | null | undefined,
|
|
56
|
+
id: string,
|
|
57
|
+
): boolean {
|
|
58
|
+
if (node?.role !== 'component') return false;
|
|
59
|
+
if (!source || typeof source.forkComponent !== 'function') return false;
|
|
60
|
+
if (typeof source.sourceLocation !== 'function' || !source.sourceLocation(id)) return false;
|
|
61
|
+
if (typeof source.definitionLocation !== 'function' || !source.definitionLocation(id)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return source.canForkComponent?.(id) !== false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ------------------------------------------------------------------ sentences
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The success hint. It NAMES THE NEW FILE, which is the whole reason this is a
|
|
71
|
+
* hint and not silence: the author has to know what was created, because undo
|
|
72
|
+
* (which owns the callsite edit) will not remove it.
|
|
73
|
+
*/
|
|
74
|
+
export function forkedHint(tag: string, newName: string, newPath: string): string {
|
|
75
|
+
return (
|
|
76
|
+
`Forked <${tag}> → <${newName}> (${newPath}). ` +
|
|
77
|
+
'Undo restores the callsite; the new file stays.'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The row lost its callsite or definition between menu and click. */
|
|
82
|
+
export function forkUnavailableHint(tag: string): string {
|
|
83
|
+
return `<${tag}> is no longer in the source index — there is nothing to fork.`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** No dev-server write backend in this session (a hosted or read-only tier). */
|
|
87
|
+
export function forkNoBackendHint(tag: string): string {
|
|
88
|
+
return `This session cannot write project source, so <${tag}> cannot be forked.`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The new file was written and the CALLSITE edit then failed. Reported in full
|
|
93
|
+
* rather than swallowed: the copy is inert (nothing imports it), so the failure
|
|
94
|
+
* mode is clutter, not breakage — and the author is told exactly that, plus the
|
|
95
|
+
* path, so they can delete it or retry.
|
|
96
|
+
*/
|
|
97
|
+
export function forkPartialHint(
|
|
98
|
+
tag: string,
|
|
99
|
+
newName: string,
|
|
100
|
+
newPath: string,
|
|
101
|
+
detail: string,
|
|
102
|
+
): string {
|
|
103
|
+
return (
|
|
104
|
+
`Wrote ${newPath} for <${newName}>, but the <${tag}> callsite was not retargeted (${detail}). ` +
|
|
105
|
+
'Nothing imports the new file yet, so it is inert — delete it or try again.'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Any other refusal: the plan's own named reason, or a transport failure. */
|
|
110
|
+
export function forkRefusedHint(reason: string): string {
|
|
111
|
+
return `Cannot fork: ${reason}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Did a hint sentence report a LANDED fork? Owned by this module — the one
|
|
115
|
+
* place the sentences are minted — so the command door's ok/error split
|
|
116
|
+
* cannot drift from the wording it matches. The extract twin is
|
|
117
|
+
* `isExtractedHint`. */
|
|
118
|
+
export function isForkedHint(hint: string): boolean {
|
|
119
|
+
return hint.startsWith('Forked ');
|
|
120
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE stable hook on the editor's play control.
|
|
3
|
+
*
|
|
4
|
+
* `PlayBar` renders two different transports — the native one (title "Play
|
|
5
|
+
* (run game)") and the ingest one (label "Run ingested game") — and anything
|
|
6
|
+
* outside the component that needs to find "the play control" was matching a
|
|
7
|
+
* LABEL. `vgai doctor` polled for the native string as its editor-readiness
|
|
8
|
+
* boundary and therefore waited out its whole timeout budget on every ingest
|
|
9
|
+
* project, which renders the other one (SimCity ingest dogfood, S-6).
|
|
10
|
+
*
|
|
11
|
+
* A label is a product decision that moves; this hook is not. Both transports
|
|
12
|
+
* carry it, so a caller asking "is the play control up?" gets one selector
|
|
13
|
+
* that is true on every surface — and adding a THIRD transport means adding
|
|
14
|
+
* the hook, not teaching every caller a third string.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately a dependency-free leaf module: `packages/editor/scripts/
|
|
17
|
+
* run-doctor.ts` runs under plain `tsx` (no Vite, no DOM, and it imports by
|
|
18
|
+
* RELATIVE path because tsx resolves the package/`@editor/*` aliases from
|
|
19
|
+
* the process CWD — only a repo-root cwd has them) and imports this directly,
|
|
20
|
+
* so it must never pull React or the design system in.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const PLAY_CONTROL_TEST_ID = 'play-control';
|
|
24
|
+
|
|
25
|
+
/** CSS selector form — what Playwright/`querySelector` callers want. */
|
|
26
|
+
export const PLAY_CONTROL_SELECTOR = `[data-testid="${PLAY_CONTROL_TEST_ID}"]`;
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "one dialect, full capability" (second bullet) — an in-page shim
|
|
3
|
+
* implementing a SUBSET of Playwright's own `Page`/`Locator` surface, rooted at
|
|
4
|
+
* an arbitrary `HTMLElement` (the play-mode game container, `play-mode.ts`'s
|
|
5
|
+
* `getGameContainer()` — see interface doctrine §3.2: "The AI should think it
|
|
6
|
+
* is basically just executing Playwright"). It runs INSIDE the already-open
|
|
7
|
+
* editor tab (no browser automation, no CDP) — `command-listener.ts`'s
|
|
8
|
+
* `page-script` relay op constructs one per call and hands it to a
|
|
9
|
+
* reconstructed step function; a real `@playwright/test` `Page` drives the SAME
|
|
10
|
+
* step file when the spec runs under `PageTransport`
|
|
11
|
+
* (`packages/vgai-live/src/game-client/client.ts`) in CI. That duality is why
|
|
12
|
+
* `createPageShim`'s return value is typed `as Page` — every implemented member
|
|
13
|
+
* matches Playwright's own shape closely enough that a step written against it
|
|
14
|
+
* compiles against the real type too.
|
|
15
|
+
*
|
|
16
|
+
* ## Honesty boundary (spec §3.2, §4 rung 4, §4's honesty-boundary bullet)
|
|
17
|
+
*
|
|
18
|
+
* This shim is for DOM interaction — menus, dialogs, HUD controls, and
|
|
19
|
+
* React-only games whose gameplay surface is the DOM itself. Its
|
|
20
|
+
* events are synthesized DOM events (`dispatchEvent`, always `isTrusted:
|
|
21
|
+
* false` — inherent to the DOM API, never faked or hidden) with no real hit
|
|
22
|
+
* testing. Canvas/adapter gameplay input must still use the engine's virtual-
|
|
23
|
+
* action seam (`game.input.*`); a React-only game uses `game.page(...)` because
|
|
24
|
+
* DOM keyboard/click handlers are its real player-facing input path.
|
|
25
|
+
*
|
|
26
|
+
* Every Playwright `Page`/`Locator` member NOT implemented below throws
|
|
27
|
+
* `PageShimUnsupportedError` naming itself and listing what IS supported —
|
|
28
|
+
* per the interface doctrine, "the illusion may be incomplete, never
|
|
29
|
+
* silently wrong": a step calling `page.mouse.move(...)` must fail loudly the
|
|
30
|
+
* instant it touches the unsupported member, not silently no-op.
|
|
31
|
+
*
|
|
32
|
+
* ## The wire's closure-capture limitation (decision 2)
|
|
33
|
+
*
|
|
34
|
+
* A `page-script` step travels the wire as `step.toString()` and is
|
|
35
|
+
* reconstructed here with `new Function('page', 'return (' + src + ')(page)')`
|
|
36
|
+
* — the SAME limitation class as Playwright's own `page.evaluate`
|
|
37
|
+
* serialization: closures over anything outside the step's own function body
|
|
38
|
+
* (outer `const`s, imported helpers, captured test state) do NOT survive the
|
|
39
|
+
* trip. Only `page` itself (this shim's return value) and whatever the step
|
|
40
|
+
* computes/inlines internally are available. Specs meant to run unmodified
|
|
41
|
+
* under both `PageTransport` (real Playwright, real closures) and
|
|
42
|
+
* `RelayTransport` (this shim, serialized) must be written as though ALWAYS
|
|
43
|
+
* serialized — inline every value the step needs; see
|
|
44
|
+
* `packages/vgai-live/src/game-client/client.ts`'s `GameClient.page()` doc comment for
|
|
45
|
+
* the client-facing half of this contract.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import type { Locator, Page } from '@playwright/test';
|
|
49
|
+
|
|
50
|
+
/** Thrown by every unimplemented `Page`/`Locator` member this shim proxies.
|
|
51
|
+
* `member` is the exact property name touched; `supported` is the sibling
|
|
52
|
+
* member list at the SAME object level (Page's own supported members, or a
|
|
53
|
+
* Locator's own), so the thrown message alone is enough for an agent to
|
|
54
|
+
* self-correct without reading this file. */
|
|
55
|
+
export class PageShimUnsupportedError extends Error {
|
|
56
|
+
readonly member: string;
|
|
57
|
+
readonly supported: readonly string[];
|
|
58
|
+
|
|
59
|
+
constructor(objectName: string, member: string, supported: readonly string[]) {
|
|
60
|
+
super(
|
|
61
|
+
`playwright-shim: ${objectName}.${member} is not supported by the in-page shim. ` +
|
|
62
|
+
`Supported ${objectName} members: ${supported.join(', ')}. ` +
|
|
63
|
+
'This shim is DOM-only — canvas gameplay input must use game.input.*, while React-only ' +
|
|
64
|
+
'DOM games may use Page keyboard/locator input. See packages/editor/src/playwright-shim.ts for ' +
|
|
65
|
+
"the full supported subset and the wire's closure-capture limitation.",
|
|
66
|
+
);
|
|
67
|
+
this.name = 'PageShimUnsupportedError';
|
|
68
|
+
this.member = member;
|
|
69
|
+
this.supported = supported;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Wraps a plain object so that reading any property NOT already present
|
|
75
|
+
* (own OR inherited — `in` walks the prototype chain, so ordinary
|
|
76
|
+
* `Object.prototype` members like `toString`/`hasOwnProperty` pass through
|
|
77
|
+
* harmlessly) throws `PageShimUnsupportedError` instead of returning
|
|
78
|
+
* `undefined`. Symbol-keyed access (`Symbol.toPrimitive`, Node's
|
|
79
|
+
* `util.inspect.custom`, etc. — console/debugger machinery that probes
|
|
80
|
+
* objects defensively) is let through as `undefined` rather than throwing,
|
|
81
|
+
* so merely logging or inspecting a shim object never itself explodes.
|
|
82
|
+
*/
|
|
83
|
+
function wrapWithUnsupportedProxy<T extends object>(target: T, objectName: string): T {
|
|
84
|
+
const supported = Object.keys(target);
|
|
85
|
+
return new Proxy(target, {
|
|
86
|
+
get(obj, prop, receiver) {
|
|
87
|
+
if (typeof prop === 'symbol') return Reflect.get(obj, prop, receiver);
|
|
88
|
+
if (prop in obj) return Reflect.get(obj, prop, receiver);
|
|
89
|
+
throw new PageShimUnsupportedError(objectName, prop, supported);
|
|
90
|
+
},
|
|
91
|
+
}) as T;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- accessible-name / role approximation (a reasonable subset, per brief) -
|
|
95
|
+
|
|
96
|
+
function normalizeWs(s: string): string {
|
|
97
|
+
return s.replace(/\s+/g, ' ').trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function impliedRole(el: Element): string | null {
|
|
101
|
+
const tag = el.tagName.toLowerCase();
|
|
102
|
+
if (tag === 'button') return 'button';
|
|
103
|
+
if (tag === 'a' && el.hasAttribute('href')) return 'link';
|
|
104
|
+
if (/^h[1-6]$/.test(tag)) return 'heading';
|
|
105
|
+
if (tag === 'textarea') return 'textbox';
|
|
106
|
+
if (tag === 'select') return 'combobox';
|
|
107
|
+
if (tag === 'img') return 'img';
|
|
108
|
+
if (tag === 'input') {
|
|
109
|
+
const type = (el.getAttribute('type') ?? 'text').toLowerCase();
|
|
110
|
+
if (type === 'button' || type === 'submit' || type === 'reset') return 'button';
|
|
111
|
+
if (type === 'checkbox') return 'checkbox';
|
|
112
|
+
if (type === 'radio') return 'radio';
|
|
113
|
+
return 'textbox';
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function roleOf(el: Element): string | null {
|
|
119
|
+
return el.getAttribute('role') ?? impliedRole(el);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Accessible-name approximation, exactly per brief: `aria-label` first, then
|
|
123
|
+
* trimmed `textContent`. Real accessible-name computation (aria-labelledby,
|
|
124
|
+
* associated `<label>`, placeholder fallback, etc.) is out of scope for this
|
|
125
|
+
* "reasonable subset". */
|
|
126
|
+
function accessibleName(el: Element): string {
|
|
127
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
128
|
+
if (ariaLabel && ariaLabel.trim()) return normalizeWs(ariaLabel);
|
|
129
|
+
return normalizeWs(el.textContent ?? '');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function matchesName(el: Element, name: string | RegExp | undefined): boolean {
|
|
133
|
+
if (name === undefined) return true;
|
|
134
|
+
const accName = accessibleName(el);
|
|
135
|
+
return name instanceof RegExp ? name.test(accName) : accName === name;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function queryByRole(
|
|
139
|
+
root: HTMLElement,
|
|
140
|
+
role: string,
|
|
141
|
+
opts?: { name?: string | RegExp },
|
|
142
|
+
): Element[] {
|
|
143
|
+
const all = Array.from(root.querySelectorAll('*'));
|
|
144
|
+
return all.filter((el) => roleOf(el) === role && matchesName(el, opts?.name));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Substring match on normalized whitespace text (Playwright's own
|
|
148
|
+
* `getByText` default), keeping only the most SPECIFIC matches — an
|
|
149
|
+
* ancestor whose own match is purely inherited from an already-matching
|
|
150
|
+
* descendant is dropped, approximating Playwright's leaf-text bias. */
|
|
151
|
+
function queryByText(root: HTMLElement, text: string | RegExp): Element[] {
|
|
152
|
+
const all = Array.from(root.querySelectorAll('*')).filter(
|
|
153
|
+
(el) => el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE',
|
|
154
|
+
);
|
|
155
|
+
const isMatch = (el: Element): boolean => {
|
|
156
|
+
const t = normalizeWs(el.textContent ?? '');
|
|
157
|
+
if (t.length === 0) return false;
|
|
158
|
+
return text instanceof RegExp ? text.test(t) : t.includes(text);
|
|
159
|
+
};
|
|
160
|
+
const matched = all.filter(isMatch);
|
|
161
|
+
return matched.filter((el) => !matched.some((other) => other !== el && el.contains(other)));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function queryByTestId(root: HTMLElement, testId: string | RegExp): Element[] {
|
|
165
|
+
return Array.from(root.querySelectorAll('[data-testid]')).filter((el) => {
|
|
166
|
+
const value = el.getAttribute('data-testid') ?? '';
|
|
167
|
+
return testId instanceof RegExp ? testId.test(value) : value === testId;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isVisible(el: Element): boolean {
|
|
172
|
+
if (!(el instanceof HTMLElement)) return true;
|
|
173
|
+
if (el.hidden) return false;
|
|
174
|
+
const style = getComputedStyle(el);
|
|
175
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- event dispatch (isTrusted:false by construction — dispatchEvent's own
|
|
179
|
+
// contract, never spoofed) -------------------------------------------------
|
|
180
|
+
|
|
181
|
+
function dispatchClick(el: Element): void {
|
|
182
|
+
const rect = (el as HTMLElement).getBoundingClientRect?.() ?? {
|
|
183
|
+
left: 0,
|
|
184
|
+
top: 0,
|
|
185
|
+
width: 0,
|
|
186
|
+
height: 0,
|
|
187
|
+
};
|
|
188
|
+
const clientX = rect.left + rect.width / 2;
|
|
189
|
+
const clientY = rect.top + rect.height / 2;
|
|
190
|
+
// `view` is deliberately omitted: Playwright's own synthesized events make
|
|
191
|
+
// no promise about it either, and supplying one risks a cross-realm
|
|
192
|
+
// mismatch (the element's owner window vs. whatever `Window` class the
|
|
193
|
+
// event constructors currently in scope expect) — omitting it sidesteps
|
|
194
|
+
// that entirely with no loss of fidelity for a UI-only automation shim.
|
|
195
|
+
const mouseInit: MouseEventInit = {
|
|
196
|
+
bubbles: true,
|
|
197
|
+
cancelable: true,
|
|
198
|
+
clientX,
|
|
199
|
+
clientY,
|
|
200
|
+
button: 0,
|
|
201
|
+
};
|
|
202
|
+
if (typeof PointerEvent !== 'undefined') {
|
|
203
|
+
el.dispatchEvent(
|
|
204
|
+
new PointerEvent('pointerdown', { ...mouseInit, pointerId: 1, isPrimary: true }),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
el.dispatchEvent(new MouseEvent('mousedown', mouseInit));
|
|
208
|
+
if (typeof PointerEvent !== 'undefined') {
|
|
209
|
+
el.dispatchEvent(
|
|
210
|
+
new PointerEvent('pointerup', { ...mouseInit, pointerId: 1, isPrimary: true }),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
el.dispatchEvent(new MouseEvent('mouseup', mouseInit));
|
|
214
|
+
el.dispatchEvent(new MouseEvent('click', mouseInit));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** React-compatible value set: React's synthetic `onChange` listens through
|
|
218
|
+
* a wrapped native setter, so assigning `el.value = ...` directly is a
|
|
219
|
+
* no-op from React's perspective — bypass it via the PROTOTYPE's own
|
|
220
|
+
* setter (the well-known trick), then fire real `input`/`change` events. */
|
|
221
|
+
function nativeValueSetter(el: HTMLInputElement | HTMLTextAreaElement): (value: string) => void {
|
|
222
|
+
const proto =
|
|
223
|
+
el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
224
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, 'value');
|
|
225
|
+
return (value: string) => {
|
|
226
|
+
if (descriptor?.set) {
|
|
227
|
+
descriptor.set.call(el, value);
|
|
228
|
+
} else {
|
|
229
|
+
el.value = value;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function dispatchFill(el: Element, value: string): void {
|
|
235
|
+
if (!(el instanceof HTMLInputElement) && !(el instanceof HTMLTextAreaElement)) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`playwright-shim: fill() target is not an <input>/<textarea> (got <${el.tagName.toLowerCase()}>)`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
el.focus();
|
|
241
|
+
nativeValueSetter(el)(value);
|
|
242
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
243
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// --- keyboard --------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
const MODIFIER_KEYS = new Set(['Alt', 'Control', 'Meta', 'Shift']);
|
|
249
|
+
|
|
250
|
+
function normalizedKey(key: string): string {
|
|
251
|
+
if (key !== 'ControlOrMeta') return key;
|
|
252
|
+
return /Mac|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Control';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function codeForKey(key: string): string {
|
|
256
|
+
if (/^[a-z]$/i.test(key)) return `Key${key.toUpperCase()}`;
|
|
257
|
+
if (/^[0-9]$/.test(key)) return `Digit${key}`;
|
|
258
|
+
if (key === ' ') return 'Space';
|
|
259
|
+
return key;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function keyboardTarget(root: HTMLElement): HTMLElement {
|
|
263
|
+
const active = document.activeElement;
|
|
264
|
+
return active instanceof HTMLElement && root.contains(active) ? active : root;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function dispatchKey(
|
|
268
|
+
root: HTMLElement,
|
|
269
|
+
type: 'keydown' | 'keyup',
|
|
270
|
+
key: string,
|
|
271
|
+
pressed: ReadonlySet<string>,
|
|
272
|
+
): void {
|
|
273
|
+
keyboardTarget(root).dispatchEvent(
|
|
274
|
+
new KeyboardEvent(type, {
|
|
275
|
+
key,
|
|
276
|
+
code: codeForKey(key),
|
|
277
|
+
bubbles: true,
|
|
278
|
+
cancelable: true,
|
|
279
|
+
altKey: pressed.has('Alt'),
|
|
280
|
+
ctrlKey: pressed.has('Control'),
|
|
281
|
+
metaKey: pressed.has('Meta'),
|
|
282
|
+
shiftKey: pressed.has('Shift'),
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function makeKeyboard(root: HTMLElement) {
|
|
288
|
+
const pressed = new Set<string>();
|
|
289
|
+
const down = async (rawKey: string): Promise<void> => {
|
|
290
|
+
const key = normalizedKey(rawKey);
|
|
291
|
+
pressed.add(key);
|
|
292
|
+
dispatchKey(root, 'keydown', key, pressed);
|
|
293
|
+
};
|
|
294
|
+
const up = async (rawKey: string): Promise<void> => {
|
|
295
|
+
const key = normalizedKey(rawKey);
|
|
296
|
+
dispatchKey(root, 'keyup', key, pressed);
|
|
297
|
+
pressed.delete(key);
|
|
298
|
+
};
|
|
299
|
+
return {
|
|
300
|
+
down,
|
|
301
|
+
up,
|
|
302
|
+
async press(shortcut: string): Promise<void> {
|
|
303
|
+
const keys = shortcut.split('+').map(normalizedKey);
|
|
304
|
+
const modifiers = keys.filter((key) => MODIFIER_KEYS.has(key));
|
|
305
|
+
const primary = keys.find((key) => !MODIFIER_KEYS.has(key));
|
|
306
|
+
for (const modifier of modifiers) await down(modifier);
|
|
307
|
+
if (primary) {
|
|
308
|
+
await down(primary);
|
|
309
|
+
await up(primary);
|
|
310
|
+
}
|
|
311
|
+
for (const modifier of modifiers.reverse()) await up(modifier);
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// --- ariaSnapshot ------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
/** A simple indented role/name tree — "enough for an agent to orient" (per
|
|
319
|
+
* brief), not a byte-accurate accessibility-tree dump. Only elements with a
|
|
320
|
+
* resolvable role appear; their descendants nest one level deeper. */
|
|
321
|
+
function buildAriaSnapshot(root: Element): string {
|
|
322
|
+
const lines: string[] = [];
|
|
323
|
+
function walk(el: Element, depth: number): void {
|
|
324
|
+
const role = roleOf(el);
|
|
325
|
+
if (role) {
|
|
326
|
+
const name = accessibleName(el);
|
|
327
|
+
lines.push(`${' '.repeat(depth)}- ${role}${name ? ` "${name}"` : ''}`);
|
|
328
|
+
}
|
|
329
|
+
const nextDepth = role ? depth + 1 : depth;
|
|
330
|
+
for (const child of Array.from(el.children)) walk(child, nextDepth);
|
|
331
|
+
}
|
|
332
|
+
for (const child of Array.from(root.children)) walk(child, 0);
|
|
333
|
+
return lines.join('\n');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// --- Locator -------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
function makeLocator(query: () => Element[], description: string): Locator {
|
|
339
|
+
const resolveFirst = (action: string): Element => {
|
|
340
|
+
const el = query()[0];
|
|
341
|
+
if (!el) {
|
|
342
|
+
throw new Error(`playwright-shim: ${action}() found no element for ${description}`);
|
|
343
|
+
}
|
|
344
|
+
return el;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const locatorObj = {
|
|
348
|
+
async click(): Promise<void> {
|
|
349
|
+
dispatchClick(resolveFirst('click'));
|
|
350
|
+
},
|
|
351
|
+
async fill(value: string): Promise<void> {
|
|
352
|
+
dispatchFill(resolveFirst('fill'), value);
|
|
353
|
+
},
|
|
354
|
+
async textContent(): Promise<string | null> {
|
|
355
|
+
const el = query()[0];
|
|
356
|
+
return el ? (el.textContent ?? null) : null;
|
|
357
|
+
},
|
|
358
|
+
async evaluate<R, Arg>(
|
|
359
|
+
pageFunction: (element: Element, arg: Arg) => R | Promise<R>,
|
|
360
|
+
arg?: Arg,
|
|
361
|
+
): Promise<R> {
|
|
362
|
+
return pageFunction(resolveFirst('evaluate'), arg as Arg);
|
|
363
|
+
},
|
|
364
|
+
async count(): Promise<number> {
|
|
365
|
+
return query().length;
|
|
366
|
+
},
|
|
367
|
+
async waitFor(opts?: { state?: 'visible' | 'attached'; timeout?: number }): Promise<void> {
|
|
368
|
+
const timeoutMs = opts?.timeout ?? 5000;
|
|
369
|
+
const wantState = opts?.state ?? 'visible';
|
|
370
|
+
const deadline = Date.now() + timeoutMs;
|
|
371
|
+
for (;;) {
|
|
372
|
+
const el = query()[0];
|
|
373
|
+
if (el && (wantState === 'attached' || isVisible(el))) return;
|
|
374
|
+
if (Date.now() >= deadline) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
`playwright-shim: waitFor() timed out after ${timeoutMs}ms waiting for ${description} ` +
|
|
377
|
+
`to be ${wantState}`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
first(): Locator {
|
|
384
|
+
return makeLocator(() => query().slice(0, 1), `${description}.first()`);
|
|
385
|
+
},
|
|
386
|
+
nth(index: number): Locator {
|
|
387
|
+
return makeLocator(() => {
|
|
388
|
+
const el = query()[index];
|
|
389
|
+
return el ? [el] : [];
|
|
390
|
+
}, `${description}.nth(${index})`);
|
|
391
|
+
},
|
|
392
|
+
// Real Playwright places `ariaSnapshot` on `Locator` (not `Page`) — kept
|
|
393
|
+
// here for parity with the real type; `createPageShim`'s own
|
|
394
|
+
// convenience `ariaSnapshot()` (see `PageShim` below) is a root-rooted
|
|
395
|
+
// shorthand for `page.locator(':scope').ariaSnapshot()`.
|
|
396
|
+
async ariaSnapshot(): Promise<string> {
|
|
397
|
+
return buildAriaSnapshot(resolveFirst('ariaSnapshot'));
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
return wrapWithUnsupportedProxy(locatorObj, 'Locator') as unknown as Locator;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// --- evaluate --------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
/** Accepts either a real function (called directly — no serialization
|
|
406
|
+
* needed since the shim already runs in-page, unlike real Playwright's
|
|
407
|
+
* cross-process `evaluate`) or a function-source string (reconstructed via
|
|
408
|
+
* `new Function`, same wire contract as the outer `page-script` step
|
|
409
|
+
* itself — see this module's own doc comment). */
|
|
410
|
+
async function evaluateImpl(fn: unknown, arg?: unknown): Promise<unknown> {
|
|
411
|
+
if (typeof fn === 'function') {
|
|
412
|
+
return (fn as (a: unknown) => unknown)(arg);
|
|
413
|
+
}
|
|
414
|
+
if (typeof fn === 'string') {
|
|
415
|
+
// Reconstructing a wire-carried function's source is this shim's
|
|
416
|
+
// documented contract (decision 2) — the same limitation class as
|
|
417
|
+
// Playwright's own evaluate serialization.
|
|
418
|
+
const reconstructed = new Function('arg', `return (${fn})(arg)`) as (a: unknown) => unknown;
|
|
419
|
+
return reconstructed(arg);
|
|
420
|
+
}
|
|
421
|
+
throw new Error('playwright-shim: evaluate() requires a function or a function-source string');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// --- Page ------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* `createPageShim`'s return type: real Playwright's `Page` plus ONE
|
|
428
|
+
* shim-specific convenience, `ariaSnapshot()` — real Playwright only places
|
|
429
|
+
* `ariaSnapshot` on `Locator` (`page.locator(...).ariaSnapshot()`), which
|
|
430
|
+
* this shim ALSO implements (see `makeLocator`, above) for byte-parity with
|
|
431
|
+
* real specs. The root-level shorthand here exists because "enough for an
|
|
432
|
+
* agent to orient" (the brief this shim was built against) is a whole-root
|
|
433
|
+
* orientation primitive, not a per-locator one — a step that calls
|
|
434
|
+
* `page.ariaSnapshot()` directly is shim-specific and won't type-check
|
|
435
|
+
* against a bare `Page` under real Playwright; prefer
|
|
436
|
+
* `page.locator(':scope').ariaSnapshot()` (or any real Locator) in a spec
|
|
437
|
+
* meant to run unmodified under both hosts.
|
|
438
|
+
*/
|
|
439
|
+
export type PageShim = Page & { ariaSnapshot(): Promise<string> };
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Builds an in-page `Page` shim rooted at `root` (the game container — never
|
|
443
|
+
* editor chrome, see this module's own doc comment). Every member not
|
|
444
|
+
* implemented here throws `PageShimUnsupportedError` the instant it is
|
|
445
|
+
* touched (property read), naming itself and the supported list.
|
|
446
|
+
*/
|
|
447
|
+
export function createPageShim(root: HTMLElement): PageShim {
|
|
448
|
+
const pageObj = {
|
|
449
|
+
keyboard: makeKeyboard(root),
|
|
450
|
+
evaluate: async (fn: unknown, arg?: unknown): Promise<unknown> => evaluateImpl(fn, arg),
|
|
451
|
+
locator: (selector: string): Locator =>
|
|
452
|
+
makeLocator(
|
|
453
|
+
() => Array.from(root.querySelectorAll(selector)),
|
|
454
|
+
`locator(${JSON.stringify(selector)})`,
|
|
455
|
+
),
|
|
456
|
+
getByRole: (role: string, opts?: { name?: string | RegExp }): Locator =>
|
|
457
|
+
makeLocator(
|
|
458
|
+
() => queryByRole(root, role, opts),
|
|
459
|
+
`getByRole(${JSON.stringify(role)}${
|
|
460
|
+
opts?.name !== undefined ? `, {name: ${JSON.stringify(String(opts.name))}}` : ''
|
|
461
|
+
})`,
|
|
462
|
+
),
|
|
463
|
+
getByText: (text: string | RegExp): Locator =>
|
|
464
|
+
makeLocator(() => queryByText(root, text), `getByText(${JSON.stringify(String(text))})`),
|
|
465
|
+
getByTestId: (testId: string | RegExp): Locator =>
|
|
466
|
+
makeLocator(
|
|
467
|
+
() => queryByTestId(root, testId),
|
|
468
|
+
`getByTestId(${JSON.stringify(String(testId))})`,
|
|
469
|
+
),
|
|
470
|
+
click: async (selector: string): Promise<void> => {
|
|
471
|
+
await pageObj.locator(selector).click();
|
|
472
|
+
},
|
|
473
|
+
fill: async (selector: string, value: string): Promise<void> => {
|
|
474
|
+
await pageObj.locator(selector).fill(value);
|
|
475
|
+
},
|
|
476
|
+
ariaSnapshot: async (): Promise<string> => buildAriaSnapshot(root),
|
|
477
|
+
};
|
|
478
|
+
return wrapWithUnsupportedProxy(pageObj, 'Page') as unknown as PageShim;
|
|
479
|
+
}
|