@forgeax/engine-app 0.0.0-dev.8d955ade1c79
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 +202 -0
- package/README.md +424 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/animation-asset-lookup.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-asset-lookup.unit.test.d.ts.map +1 -0
- package/dist/__tests__/callback-deletion.unit.test.d.ts +2 -0
- package/dist/__tests__/callback-deletion.unit.test.d.ts.map +1 -0
- package/dist/__tests__/callback-inference.test-d.d.ts +2 -0
- package/dist/__tests__/callback-inference.test-d.d.ts.map +1 -0
- package/dist/__tests__/component-introspection.unit.test.d.ts +2 -0
- package/dist/__tests__/component-introspection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/consumer-migration.unit.test.d.ts +2 -0
- package/dist/__tests__/consumer-migration.unit.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-error-retention.integration.test.d.ts +2 -0
- package/dist/__tests__/create-app-error-retention.integration.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-features.test.d.ts +2 -0
- package/dist/__tests__/create-app-features.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-listener-sync.test.d.ts +2 -0
- package/dist/__tests__/create-app-listener-sync.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-lock-provider.test.d.ts +2 -0
- package/dist/__tests__/create-app-lock-provider.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts +2 -0
- package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-plugin.test.d.ts +2 -0
- package/dist/__tests__/create-app-plugin.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-stop.test.d.ts +2 -0
- package/dist/__tests__/create-app-stop.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-time.unit.test.d.ts +2 -0
- package/dist/__tests__/create-app-time.unit.test.d.ts.map +1 -0
- package/dist/__tests__/create-app.test-d.d.ts +2 -0
- package/dist/__tests__/create-app.test-d.d.ts.map +1 -0
- package/dist/__tests__/create-app.test.d.ts +2 -0
- package/dist/__tests__/create-app.test.d.ts.map +1 -0
- package/dist/__tests__/create-renderer-error.integration.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-error.integration.test.d.ts.map +1 -0
- package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts.map +1 -0
- package/dist/__tests__/docs-gate-green.unit.test.d.ts +2 -0
- package/dist/__tests__/docs-gate-green.unit.test.d.ts.map +1 -0
- package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts +2 -0
- package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts.map +1 -0
- package/dist/__tests__/draw-source-single-world-regression.test.d.ts +2 -0
- package/dist/__tests__/draw-source-single-world-regression.test.d.ts.map +1 -0
- package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts +2 -0
- package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts.map +1 -0
- package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts +2 -0
- package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors-pointer-lock-failed.test.d.ts +2 -0
- package/dist/__tests__/errors-pointer-lock-failed.test.d.ts.map +1 -0
- package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-error-detail.test-d.d.ts +2 -0
- package/dist/__tests__/execution-error-detail.test-d.d.ts.map +1 -0
- package/dist/__tests__/execution-error-detail.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-error-detail.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-frame-order.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-frame-order.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-frame-protocol.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-frame-protocol.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-input-sample.contract.test.d.ts +2 -0
- package/dist/__tests__/execution-input-sample.contract.test.d.ts.map +1 -0
- package/dist/__tests__/execution-measurement.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-measurement.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-public-api.test-d.d.ts +2 -0
- package/dist/__tests__/execution-public-api.test-d.d.ts.map +1 -0
- package/dist/__tests__/execution-public-api.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-public-api.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-rebuild.contract.test.d.ts +2 -0
- package/dist/__tests__/execution-rebuild.contract.test.d.ts.map +1 -0
- package/dist/__tests__/execution-report.contract.test.d.ts +2 -0
- package/dist/__tests__/execution-report.contract.test.d.ts.map +1 -0
- package/dist/__tests__/execution-report.types.test-d.d.ts +2 -0
- package/dist/__tests__/execution-report.types.test-d.d.ts.map +1 -0
- package/dist/__tests__/execution-schema-freshness.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-schema-freshness.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-selector.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-selector.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-shared-kernel.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-shared-kernel.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fallback-camera.unit.test.d.ts +2 -0
- package/dist/__tests__/fallback-camera.unit.test.d.ts.map +1 -0
- package/dist/__tests__/frame-loop-time-elapsed.test.d.ts +2 -0
- package/dist/__tests__/frame-loop-time-elapsed.test.d.ts.map +1 -0
- package/dist/__tests__/frame-loop-time.unit.test.d.ts +2 -0
- package/dist/__tests__/frame-loop-time.unit.test.d.ts.map +1 -0
- package/dist/__tests__/frame-loop-world-array.test.d.ts +2 -0
- package/dist/__tests__/frame-loop-world-array.test.d.ts.map +1 -0
- package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts +2 -0
- package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts.map +1 -0
- package/dist/__tests__/migration-surface-inventory.unit.test.d.ts +2 -0
- package/dist/__tests__/migration-surface-inventory.unit.test.d.ts.map +1 -0
- package/dist/__tests__/plugin-policy.integration.test.d.ts +2 -0
- package/dist/__tests__/plugin-policy.integration.test.d.ts.map +1 -0
- package/dist/__tests__/plugin-profiles.unit.test.d.ts +2 -0
- package/dist/__tests__/plugin-profiles.unit.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-default-off.test.d.ts +2 -0
- package/dist/__tests__/profiler-default-off.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-frame-token.test.d.ts +2 -0
- package/dist/__tests__/profiler-frame-token.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-phase-catalog.test.d.ts +2 -0
- package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-phase-integration.test.d.ts +2 -0
- package/dist/__tests__/profiler-phase-integration.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-remote-bridge.test.d.ts +2 -0
- package/dist/__tests__/profiler-remote-bridge.test.d.ts.map +1 -0
- package/dist/__tests__/remote-integration.test.d.ts +2 -0
- package/dist/__tests__/remote-integration.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-capture-capability.test-d.d.ts +2 -0
- package/dist/__tests__/rhi-capture-capability.test-d.d.ts.map +1 -0
- package/dist/__tests__/rhi-capture-host.integration.test.d.ts +2 -0
- package/dist/__tests__/rhi-capture-host.integration.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-capture.browser.test.d.ts +2 -0
- package/dist/__tests__/rhi-capture.browser.test.d.ts.map +1 -0
- package/dist/__tests__/surface-handoff.test.d.ts +2 -0
- package/dist/__tests__/surface-handoff.test.d.ts.map +1 -0
- package/dist/__tests__/types-m2.test-d.d.ts +2 -0
- package/dist/__tests__/types-m2.test-d.d.ts.map +1 -0
- package/dist/__tests__/visibility-remote.integration.test.d.ts +2 -0
- package/dist/__tests__/visibility-remote.integration.test.d.ts.map +1 -0
- package/dist/__tests__/worker-execution-stop.test.d.ts +2 -0
- package/dist/__tests__/worker-execution-stop.test.d.ts.map +1 -0
- package/dist/animation-asset-lookup.d.ts +7 -0
- package/dist/animation-asset-lookup.d.ts.map +1 -0
- package/dist/browser-frame-signal.d.ts +17 -0
- package/dist/browser-frame-signal.d.ts.map +1 -0
- package/dist/canvas-policy.d.ts +4 -0
- package/dist/canvas-policy.d.ts.map +1 -0
- package/dist/create-app.d.ts +78 -0
- package/dist/create-app.d.ts.map +1 -0
- package/dist/engine-worker-runtime.mjs +827 -0
- package/dist/engine-worker-runtime.mjs.map +1 -0
- package/dist/errors.d.ts +254 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/execution/attached-world-swap.d.ts +10 -0
- package/dist/execution/attached-world-swap.d.ts.map +1 -0
- package/dist/execution/bootstrap-entry.d.ts +30 -0
- package/dist/execution/bootstrap-entry.d.ts.map +1 -0
- package/dist/execution/bootstrap-url.d.ts +5 -0
- package/dist/execution/bootstrap-url.d.ts.map +1 -0
- package/dist/execution/capabilities.d.ts +5 -0
- package/dist/execution/capabilities.d.ts.map +1 -0
- package/dist/execution/control.d.ts +12 -0
- package/dist/execution/control.d.ts.map +1 -0
- package/dist/execution/engine-worker-runtime.d.ts +2 -0
- package/dist/execution/engine-worker-runtime.d.ts.map +1 -0
- package/dist/execution/engine-worker.d.ts +24 -0
- package/dist/execution/engine-worker.d.ts.map +1 -0
- package/dist/execution/host-controller.d.ts +16 -0
- package/dist/execution/host-controller.d.ts.map +1 -0
- package/dist/execution/index.d.ts +10 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/kernel-pool.d.ts +14 -0
- package/dist/execution/kernel-pool.d.ts.map +1 -0
- package/dist/execution/kernel-worker-runtime.d.ts +2 -0
- package/dist/execution/kernel-worker-runtime.d.ts.map +1 -0
- package/dist/execution/measurement.d.ts +7 -0
- package/dist/execution/measurement.d.ts.map +1 -0
- package/dist/execution/protocol.d.ts +87 -0
- package/dist/execution/protocol.d.ts.map +1 -0
- package/dist/execution/report.d.ts +5 -0
- package/dist/execution/report.d.ts.map +1 -0
- package/dist/execution/schema.d.ts +4 -0
- package/dist/execution/schema.d.ts.map +1 -0
- package/dist/execution/selector.d.ts +10 -0
- package/dist/execution/selector.d.ts.map +1 -0
- package/dist/execution/types.d.ts +123 -0
- package/dist/execution/types.d.ts.map +1 -0
- package/dist/fallback-camera.d.ts +8 -0
- package/dist/fallback-camera.d.ts.map +1 -0
- package/dist/game-context.d.ts +98 -0
- package/dist/game-context.d.ts.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3980 -0
- package/dist/index.mjs.map +1 -0
- package/dist/input-map-plugin.d.ts +5 -0
- package/dist/input-map-plugin.d.ts.map +1 -0
- package/dist/input-plugin.d.ts +4 -0
- package/dist/input-plugin.d.ts.map +1 -0
- package/dist/internal/assembled-engine-profile.d.ts +5 -0
- package/dist/internal/assembled-engine-profile.d.ts.map +1 -0
- package/dist/internal/assets-world-plugin.d.ts +7 -0
- package/dist/internal/assets-world-plugin.d.ts.map +1 -0
- package/dist/internal/browser-remote-bridge.d.ts +24 -0
- package/dist/internal/browser-remote-bridge.d.ts.map +1 -0
- package/dist/internal/browser-rhi-debug-runtime.d.ts +18 -0
- package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -0
- package/dist/internal/component-introspection.d.ts +24 -0
- package/dist/internal/component-introspection.d.ts.map +1 -0
- package/dist/internal/debug-draw.d.ts +9 -0
- package/dist/internal/debug-draw.d.ts.map +1 -0
- package/dist/internal/ecs-import.d.ts +6 -0
- package/dist/internal/ecs-import.d.ts.map +1 -0
- package/dist/internal/engine-profile-common.d.ts +9 -0
- package/dist/internal/engine-profile-common.d.ts.map +1 -0
- package/dist/internal/error-fanout.d.ts +66 -0
- package/dist/internal/error-fanout.d.ts.map +1 -0
- package/dist/internal/frame-loop.d.ts +38 -0
- package/dist/internal/frame-loop.d.ts.map +1 -0
- package/dist/internal/input-attach.d.ts +44 -0
- package/dist/internal/input-attach.d.ts.map +1 -0
- package/dist/internal/main-engine-profile.d.ts +19 -0
- package/dist/internal/main-engine-profile.d.ts.map +1 -0
- package/dist/internal/remote-serve-flag.d.ts +19 -0
- package/dist/internal/remote-serve-flag.d.ts.map +1 -0
- package/dist/internal/remote-server-plugin.d.ts +8 -0
- package/dist/internal/remote-server-plugin.d.ts.map +1 -0
- package/dist/internal/rhi-capture.d.ts +19 -0
- package/dist/internal/rhi-capture.d.ts.map +1 -0
- package/dist/internal/rhi-debug-flag.d.ts +18 -0
- package/dist/internal/rhi-debug-flag.d.ts.map +1 -0
- package/dist/internal/worker-engine-profile.d.ts +15 -0
- package/dist/internal/worker-engine-profile.d.ts.map +1 -0
- package/dist/kernel-worker-runtime.mjs +69 -0
- package/dist/kernel-worker-runtime.mjs.map +1 -0
- package/dist/load-game-errors.d.ts +131 -0
- package/dist/load-game-errors.d.ts.map +1 -0
- package/dist/load-game.d.ts +34 -0
- package/dist/load-game.d.ts.map +1 -0
- package/dist/plugin-factories.d.ts +3 -0
- package/dist/plugin-factories.d.ts.map +1 -0
- package/dist/renderer-plugin.d.ts +25 -0
- package/dist/renderer-plugin.d.ts.map +1 -0
- package/dist/tool-preview/bootstrap.d.ts +113 -0
- package/dist/tool-preview/bootstrap.d.ts.map +1 -0
- package/dist/tool-preview/evidence.d.ts +20 -0
- package/dist/tool-preview/evidence.d.ts.map +1 -0
- package/dist/tool-preview/framing.d.ts +13 -0
- package/dist/tool-preview/framing.d.ts.map +1 -0
- package/dist/tool-preview/recipe.d.ts +54 -0
- package/dist/tool-preview/recipe.d.ts.map +1 -0
- package/dist/types.d.ts +404 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +85 -0
- package/schema/execution-report.schema.json +168 -0
- package/src/__tests__/animation-asset-lookup.unit.test.ts +20 -0
- package/src/__tests__/callback-deletion.unit.test.ts +14 -0
- package/src/__tests__/callback-inference.test-d.ts +36 -0
- package/src/__tests__/component-introspection.unit.test.ts +56 -0
- package/src/__tests__/consumer-migration.unit.test.ts +34 -0
- package/src/__tests__/create-app-error-retention.integration.test.ts +125 -0
- package/src/__tests__/create-app-features.test.ts +51 -0
- package/src/__tests__/create-app-listener-sync.test.ts +381 -0
- package/src/__tests__/create-app-lock-provider.test.ts +212 -0
- package/src/__tests__/create-app-physics-plugin-failure.integration.test.ts +142 -0
- package/src/__tests__/create-app-plugin.test.ts +217 -0
- package/src/__tests__/create-app-stop.test.ts +609 -0
- package/src/__tests__/create-app-time.unit.test.ts +26 -0
- package/src/__tests__/create-app.test-d.ts +83 -0
- package/src/__tests__/create-app.test.ts +287 -0
- package/src/__tests__/create-renderer-error.integration.test.ts +25 -0
- package/src/__tests__/create-renderer-lifecycle.integration.test.ts +24 -0
- package/src/__tests__/docs-gate-green.unit.test.ts +61 -0
- package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +176 -0
- package/src/__tests__/draw-source-single-world-regression.test.ts +184 -0
- package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +82 -0
- package/src/__tests__/engine-worker-world-swap.unit.test.ts +69 -0
- package/src/__tests__/errors-pointer-lock-failed.test.ts +265 -0
- package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +79 -0
- package/src/__tests__/execution-error-detail.test-d.ts +13 -0
- package/src/__tests__/execution-error-detail.unit.test.ts +35 -0
- package/src/__tests__/execution-frame-order.unit.test.ts +38 -0
- package/src/__tests__/execution-frame-protocol.unit.test.ts +60 -0
- package/src/__tests__/execution-input-sample.contract.test.ts +23 -0
- package/src/__tests__/execution-measurement.unit.test.ts +20 -0
- package/src/__tests__/execution-public-api.test-d.ts +14 -0
- package/src/__tests__/execution-public-api.unit.test.ts +9 -0
- package/src/__tests__/execution-rebuild.contract.test.ts +36 -0
- package/src/__tests__/execution-report.contract.test.ts +50 -0
- package/src/__tests__/execution-report.types.test-d.ts +16 -0
- package/src/__tests__/execution-schema-freshness.unit.test.ts +20 -0
- package/src/__tests__/execution-selector.unit.test.ts +72 -0
- package/src/__tests__/execution-shared-kernel.unit.test.ts +180 -0
- package/src/__tests__/fallback-camera.unit.test.ts +32 -0
- package/src/__tests__/frame-loop-time-elapsed.test.ts +115 -0
- package/src/__tests__/frame-loop-time.unit.test.ts +61 -0
- package/src/__tests__/frame-loop-world-array.test.ts +450 -0
- package/src/__tests__/kernel-worker-runtime.unit.test.ts +50 -0
- package/src/__tests__/migration-surface-inventory.unit.test.ts +13 -0
- package/src/__tests__/plugin-policy.integration.test.ts +15 -0
- package/src/__tests__/plugin-profiles.unit.test.ts +190 -0
- package/src/__tests__/profiler-default-off.test.ts +53 -0
- package/src/__tests__/profiler-frame-token.test.ts +235 -0
- package/src/__tests__/profiler-phase-catalog.test.ts +30 -0
- package/src/__tests__/profiler-phase-integration.test.ts +117 -0
- package/src/__tests__/profiler-remote-bridge.test.ts +31 -0
- package/src/__tests__/remote-integration.test.ts +224 -0
- package/src/__tests__/rhi-capture-capability.test-d.ts +22 -0
- package/src/__tests__/rhi-capture-host.integration.test.ts +157 -0
- package/src/__tests__/rhi-capture.browser.test.ts +51 -0
- package/src/__tests__/surface-handoff.test.ts +89 -0
- package/src/__tests__/types-m2.test-d.ts +81 -0
- package/src/__tests__/visibility-remote.integration.test.ts +116 -0
- package/src/__tests__/worker-execution-stop.test.ts +173 -0
- package/src/animation-asset-lookup.ts +13 -0
- package/src/browser-frame-signal.ts +44 -0
- package/src/canvas-policy.ts +16 -0
- package/src/create-app.ts +1264 -0
- package/src/errors.ts +442 -0
- package/src/execution/attached-world-swap.ts +33 -0
- package/src/execution/bootstrap-entry.ts +122 -0
- package/src/execution/bootstrap-url.ts +25 -0
- package/src/execution/capabilities.ts +102 -0
- package/src/execution/control.ts +48 -0
- package/src/execution/engine-worker-runtime.ts +383 -0
- package/src/execution/engine-worker.ts +163 -0
- package/src/execution/host-controller.ts +460 -0
- package/src/execution/index.ts +44 -0
- package/src/execution/kernel-pool.ts +236 -0
- package/src/execution/kernel-worker-runtime.ts +103 -0
- package/src/execution/measurement.ts +39 -0
- package/src/execution/protocol.ts +134 -0
- package/src/execution/report.ts +66 -0
- package/src/execution/schema.ts +168 -0
- package/src/execution/selector.ts +98 -0
- package/src/execution/types.ts +163 -0
- package/src/fallback-camera.ts +20 -0
- package/src/game-context.ts +126 -0
- package/src/index.ts +190 -0
- package/src/input-map-plugin.ts +21 -0
- package/src/input-plugin.ts +26 -0
- package/src/internal/assembled-engine-profile.ts +14 -0
- package/src/internal/assets-world-plugin.ts +33 -0
- package/src/internal/browser-remote-bridge.ts +269 -0
- package/src/internal/browser-rhi-debug-runtime.ts +67 -0
- package/src/internal/component-introspection.ts +116 -0
- package/src/internal/debug-draw.ts +38 -0
- package/src/internal/ecs-import.ts +41 -0
- package/src/internal/engine-profile-common.ts +9 -0
- package/src/internal/error-fanout.ts +129 -0
- package/src/internal/frame-loop.ts +539 -0
- package/src/internal/input-attach.ts +104 -0
- package/src/internal/main-engine-profile.ts +84 -0
- package/src/internal/remote-serve-flag.ts +41 -0
- package/src/internal/remote-server-plugin.ts +16 -0
- package/src/internal/rhi-capture.ts +211 -0
- package/src/internal/rhi-debug-flag.ts +40 -0
- package/src/internal/worker-engine-profile.ts +47 -0
- package/src/load-game-errors.ts +211 -0
- package/src/load-game.ts +115 -0
- package/src/plugin-factories.ts +2 -0
- package/src/renderer-plugin.ts +96 -0
- package/src/tool-preview/bootstrap.ts +835 -0
- package/src/tool-preview/evidence.ts +59 -0
- package/src/tool-preview/framing.ts +52 -0
- package/src/tool-preview/recipe.ts +137 -0
- package/src/types.ts +450 -0
- package/src/vite-env.d.ts +10 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// feat-20260619-audio-resource-ownership-deterministic-reclaim / M7 / w24 fix-up.
|
|
2
|
+
//
|
|
3
|
+
// Listener sync real-consumption-path test: goes through the canvas form
|
|
4
|
+
// (createApp(canvas, opts)) so the w25 auto-registration code
|
|
5
|
+
// (create-app.ts:517-538) is actually executed. The prior version of this
|
|
6
|
+
// test used the assemble form with a hand-copied registerListenerSyncSystem
|
|
7
|
+
// helper — a shadow copy of the production code that left w25 untested.
|
|
8
|
+
//
|
|
9
|
+
// Approach: vi.mock constructRuntimeRendererHost (only construction overridden) so the canvas
|
|
10
|
+
// form does not attempt real WebGPU init; all other exports from
|
|
11
|
+
// @forgeax/engine-runtime remain real (Transform, registerPropagateTransforms,
|
|
12
|
+
// PROPAGATE_TRANSFORMS_SYSTEM, etc.). globalThis.AudioContext is stubbed
|
|
13
|
+
// with a mock whose .listener carries nine { value } AudioParam objects
|
|
14
|
+
// that syncListenerFromWorldMatrix writes to — the same nine fields the
|
|
15
|
+
// production code accesses. requestAnimationFrame is stubbed (same pattern
|
|
16
|
+
// as create-app-stop.test.ts) so start() runs one frame synchronously.
|
|
17
|
+
//
|
|
18
|
+
// Three scenarios (semantics unchanged from the prior version, but now
|
|
19
|
+
// exercising the real production path through the canvas form):
|
|
20
|
+
// (a) AudioListener+Transform entity at (5,0,0) — after start()+frame
|
|
21
|
+
// the mock listener.positionX.value === 5 (proves D-7 real addSystem
|
|
22
|
+
// auto-registration consumes the world mat4 from propagateTransforms).
|
|
23
|
+
// (b) No AudioListener entity — system no-ops, no throw, mock listener
|
|
24
|
+
// values remain at 0.
|
|
25
|
+
// (c) Frame-order regression guard (R-listener-frame-order): modify
|
|
26
|
+
// Transform.posX in the same frame-cycle and run another frame —
|
|
27
|
+
// listener pose reflects the CURRENT frame's world mat4, not a
|
|
28
|
+
// stale/previous value (proves after-propagate frame order is
|
|
29
|
+
// effective and the system does not suffer 1-frame lag).
|
|
30
|
+
|
|
31
|
+
import { AudioListener as AudioListenerComponent, audioPlugin } from '@forgeax/engine-audio';
|
|
32
|
+
import { webAudioPlugin } from '@forgeax/engine-audio-webaudio';
|
|
33
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
34
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
35
|
+
import { createApp } from '../create-app';
|
|
36
|
+
|
|
37
|
+
const constructRuntimeRendererHost = vi.hoisted(() => vi.fn());
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Mock constructRuntimeRendererHost so the canvas form does not attempt real WebGPU init.
|
|
41
|
+
// All other @forgeax/engine-runtime exports stay real (Transform,
|
|
42
|
+
// registerPropagateTransforms, advanceAnimationPlayer, etc.).
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
vi.mock('@forgeax/engine-runtime', async (importOriginal) => {
|
|
46
|
+
const actual = await importOriginal<typeof import('@forgeax/engine-runtime')>();
|
|
47
|
+
const lease = { dispose: vi.fn() };
|
|
48
|
+
const rendererStub = {
|
|
49
|
+
attach(): { ok: true; value: object } {
|
|
50
|
+
return { ok: true, value: lease };
|
|
51
|
+
},
|
|
52
|
+
draw(): { ok: true; value: object } {
|
|
53
|
+
return { ok: true, value: {} };
|
|
54
|
+
},
|
|
55
|
+
observe: async () => ({ ok: true as const, value: {} }),
|
|
56
|
+
releaseSurface: () => ({ ok: true as const, value: undefined }),
|
|
57
|
+
restoreSurface: () => ({ ok: true as const, value: undefined }),
|
|
58
|
+
recover: async () => ({ ok: true as const, value: undefined }),
|
|
59
|
+
subscribe(): () => void {
|
|
60
|
+
return () => {};
|
|
61
|
+
},
|
|
62
|
+
dispose: vi.fn(),
|
|
63
|
+
};
|
|
64
|
+
constructRuntimeRendererHost.mockResolvedValue({
|
|
65
|
+
ok: true,
|
|
66
|
+
value: { renderer: rendererStub, assets: undefined },
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
...actual,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
vi.mock('@forgeax/engine-runtime/internal/renderer-host', () => ({
|
|
74
|
+
constructRuntimeRendererHost,
|
|
75
|
+
loadRhiPack: vi.fn(),
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Test helpers
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build a mock AudioContext whose .listener carries the nine AudioParam
|
|
84
|
+
* objects (positionX/Y/Z, forwardX/Y/Z, upX/Y/Z) that
|
|
85
|
+
* syncListenerFromWorldMatrix writes to. The mock also carries the minimum
|
|
86
|
+
* surface WebAudioEngine.ensureContext() needs: createGain, destination,
|
|
87
|
+
* state, close, and the gesture-listener methods.
|
|
88
|
+
*/
|
|
89
|
+
function makeMockAudioContextForListenerSync(): AudioContext {
|
|
90
|
+
const mkGain = vi.fn(
|
|
91
|
+
() =>
|
|
92
|
+
({
|
|
93
|
+
gain: { value: 1 } as unknown as AudioParam,
|
|
94
|
+
context: undefined as unknown as BaseAudioContext,
|
|
95
|
+
numberOfInputs: 1,
|
|
96
|
+
numberOfOutputs: 1,
|
|
97
|
+
channelCount: 2,
|
|
98
|
+
channelCountMode: 'max' as const,
|
|
99
|
+
channelInterpretation: 'speakers' as const,
|
|
100
|
+
connect: vi.fn().mockReturnValue(undefined),
|
|
101
|
+
disconnect: vi.fn(),
|
|
102
|
+
addEventListener: vi.fn(),
|
|
103
|
+
removeEventListener: vi.fn(),
|
|
104
|
+
dispatchEvent: vi.fn(),
|
|
105
|
+
}) as unknown as GainNode,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const mkListener = (): AudioListener =>
|
|
109
|
+
({
|
|
110
|
+
positionX: { value: 0 } as unknown as AudioParam,
|
|
111
|
+
positionY: { value: 0 } as unknown as AudioParam,
|
|
112
|
+
positionZ: { value: 0 } as unknown as AudioParam,
|
|
113
|
+
forwardX: { value: 0 } as unknown as AudioParam,
|
|
114
|
+
forwardY: { value: 0 } as unknown as AudioParam,
|
|
115
|
+
forwardZ: { value: 0 } as unknown as AudioParam,
|
|
116
|
+
upX: { value: 0 } as unknown as AudioParam,
|
|
117
|
+
upY: { value: 0 } as unknown as AudioParam,
|
|
118
|
+
upZ: { value: 0 } as unknown as AudioParam,
|
|
119
|
+
}) as unknown as AudioListener;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
listener: mkListener(),
|
|
123
|
+
state: 'suspended' as AudioContextState,
|
|
124
|
+
sampleRate: 48000,
|
|
125
|
+
destination: {
|
|
126
|
+
maxChannelCount: 2,
|
|
127
|
+
channelCount: 2,
|
|
128
|
+
channelCountMode: 'explicit' as const,
|
|
129
|
+
channelInterpretation: 'speakers' as const,
|
|
130
|
+
context: undefined as unknown as BaseAudioContext,
|
|
131
|
+
numberOfInputs: 1,
|
|
132
|
+
numberOfOutputs: 0,
|
|
133
|
+
connect: vi.fn().mockReturnValue(undefined),
|
|
134
|
+
disconnect: vi.fn(),
|
|
135
|
+
addEventListener: vi.fn(),
|
|
136
|
+
removeEventListener: vi.fn(),
|
|
137
|
+
dispatchEvent: vi.fn(),
|
|
138
|
+
} as unknown as AudioDestinationNode,
|
|
139
|
+
currentTime: 0,
|
|
140
|
+
resume: vi.fn().mockResolvedValue(undefined),
|
|
141
|
+
suspend: vi.fn().mockResolvedValue(undefined),
|
|
142
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
143
|
+
createGain: mkGain,
|
|
144
|
+
createBufferSource: vi.fn(),
|
|
145
|
+
createPanner: vi.fn(),
|
|
146
|
+
decodeAudioData: vi.fn(),
|
|
147
|
+
addEventListener: vi.fn(),
|
|
148
|
+
removeEventListener: vi.fn(),
|
|
149
|
+
dispatchEvent: vi.fn(),
|
|
150
|
+
onstatechange: null,
|
|
151
|
+
baseLatency: 0,
|
|
152
|
+
outputLatency: 0,
|
|
153
|
+
getOutputTimestamp: vi.fn(),
|
|
154
|
+
createMediaStreamDestination: vi.fn(),
|
|
155
|
+
createMediaStreamSource: vi.fn(),
|
|
156
|
+
createChannelMerger: vi.fn(),
|
|
157
|
+
createChannelSplitter: vi.fn(),
|
|
158
|
+
createDelay: vi.fn(),
|
|
159
|
+
createBiquadFilter: vi.fn(),
|
|
160
|
+
createConvolver: vi.fn(),
|
|
161
|
+
createDynamicsCompressor: vi.fn(),
|
|
162
|
+
createOscillator: vi.fn(),
|
|
163
|
+
createStereoPanner: vi.fn(),
|
|
164
|
+
createWaveShaper: vi.fn(),
|
|
165
|
+
createIIRFilter: vi.fn(),
|
|
166
|
+
createScriptProcessor: vi.fn(),
|
|
167
|
+
createAnalyser: vi.fn(),
|
|
168
|
+
createConstantSource: vi.fn(),
|
|
169
|
+
audioWorklet: undefined as unknown as AudioWorklet,
|
|
170
|
+
} as unknown as AudioContext;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Node-env rAF injection: createFrameLoop's resolveRaf reads
|
|
175
|
+
* globalThis.requestAnimationFrame at construction time (frame-loop.ts:141-153).
|
|
176
|
+
* Call this BEFORE createApp() so the frame loop captures the stub.
|
|
177
|
+
*
|
|
178
|
+
* Auto-invoke only on first raf call (from start()), not on subsequent
|
|
179
|
+
* calls (from tick()'s `raf(tick)` in frame-loop), to avoid infinite
|
|
180
|
+
* recursion. The stub state is returned so tests can read tickFn to drive
|
|
181
|
+
* additional frames.
|
|
182
|
+
*/
|
|
183
|
+
function installRafStub(): { tickFn: (() => void) | null } {
|
|
184
|
+
const stub: { tickFn: (() => void) | null } = { tickFn: null };
|
|
185
|
+
let firstCall = true;
|
|
186
|
+
(globalThis as Record<string, unknown>).requestAnimationFrame = (cb: (t: number) => void) => {
|
|
187
|
+
stub.tickFn = () => cb(performance.now());
|
|
188
|
+
if (firstCall) {
|
|
189
|
+
firstCall = false;
|
|
190
|
+
stub.tickFn();
|
|
191
|
+
}
|
|
192
|
+
return 1;
|
|
193
|
+
};
|
|
194
|
+
(globalThis as Record<string, unknown>).cancelAnimationFrame = () => {
|
|
195
|
+
stub.tickFn = null;
|
|
196
|
+
};
|
|
197
|
+
return stub;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function uninstallRafStub() {
|
|
201
|
+
delete (globalThis as Record<string, unknown>).requestAnimationFrame;
|
|
202
|
+
delete (globalThis as Record<string, unknown>).cancelAnimationFrame;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Tests
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
describe('feat-20260619 M7: listener sync real consumption path (canvas form)', () => {
|
|
210
|
+
describe('w24 scenario (a): AudioListener+Transform entity syncs world translation', () => {
|
|
211
|
+
it('canvas form audio:true → after start() mock listener positionX === 5', async () => {
|
|
212
|
+
const OriginalAudioContext = globalThis.AudioContext;
|
|
213
|
+
vi.stubGlobal('document', {
|
|
214
|
+
addEventListener: vi.fn(),
|
|
215
|
+
removeEventListener: vi.fn(),
|
|
216
|
+
});
|
|
217
|
+
let context: AudioContext | undefined;
|
|
218
|
+
const mockCtor = vi.fn(function AudioContextMock(this: Record<string, unknown>) {
|
|
219
|
+
context = makeMockAudioContextForListenerSync();
|
|
220
|
+
Object.assign(this, context);
|
|
221
|
+
});
|
|
222
|
+
// biome-ignore lint/suspicious/noExplicitAny: vitest mock for AudioContext constructor -- must use function (not arrow) so new AudioContext() is constructable
|
|
223
|
+
globalThis.AudioContext = mockCtor as any;
|
|
224
|
+
|
|
225
|
+
installRafStub();
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const canvas = {
|
|
229
|
+
tagName: 'canvas',
|
|
230
|
+
isConnected: true,
|
|
231
|
+
width: 800,
|
|
232
|
+
height: 600,
|
|
233
|
+
} as HTMLCanvasElement;
|
|
234
|
+
|
|
235
|
+
const result = await createApp(canvas, { plugins: [webAudioPlugin(), audioPlugin()] });
|
|
236
|
+
expect(result.ok).toBe(true);
|
|
237
|
+
if (!result.ok) return;
|
|
238
|
+
const app = result.value;
|
|
239
|
+
|
|
240
|
+
// Spawn entity AFTER createApp but BEFORE start(), so the listener-sync
|
|
241
|
+
// system (registered during createApp) finds it on the first frame.
|
|
242
|
+
app.world.spawn(
|
|
243
|
+
{ component: Transform, data: { pos: [5, 0, 0] } },
|
|
244
|
+
{ component: AudioListenerComponent, data: {} },
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
app.start();
|
|
248
|
+
|
|
249
|
+
const listener = context?.listener;
|
|
250
|
+
if (!listener) throw new Error('expected host AudioContext.listener');
|
|
251
|
+
expect(listener.positionX.value).toBe(5);
|
|
252
|
+
expect(listener.positionY.value).toBe(0);
|
|
253
|
+
expect(listener.positionZ.value).toBe(0);
|
|
254
|
+
|
|
255
|
+
app.stop();
|
|
256
|
+
} finally {
|
|
257
|
+
uninstallRafStub();
|
|
258
|
+
globalThis.AudioContext = OriginalAudioContext;
|
|
259
|
+
vi.restoreAllMocks();
|
|
260
|
+
vi.unstubAllGlobals();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('w24 scenario (b): no AudioListener entity → no-op, no throw', () => {
|
|
266
|
+
it('canvas form audio:true with no AudioListener entity → start() does not throw', async () => {
|
|
267
|
+
const OriginalAudioContext = globalThis.AudioContext;
|
|
268
|
+
vi.stubGlobal('document', {
|
|
269
|
+
addEventListener: vi.fn(),
|
|
270
|
+
removeEventListener: vi.fn(),
|
|
271
|
+
});
|
|
272
|
+
const mockCtor = vi.fn(function AudioContextMock(this: Record<string, unknown>) {
|
|
273
|
+
Object.assign(this, makeMockAudioContextForListenerSync());
|
|
274
|
+
});
|
|
275
|
+
// biome-ignore lint/suspicious/noExplicitAny: vitest mock for AudioContext constructor -- must use function (not arrow) so new AudioContext() is constructable
|
|
276
|
+
globalThis.AudioContext = mockCtor as any;
|
|
277
|
+
|
|
278
|
+
installRafStub();
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const canvas = {
|
|
282
|
+
tagName: 'canvas',
|
|
283
|
+
isConnected: true,
|
|
284
|
+
width: 800,
|
|
285
|
+
height: 600,
|
|
286
|
+
} as HTMLCanvasElement;
|
|
287
|
+
|
|
288
|
+
const result = await createApp(canvas, { plugins: [webAudioPlugin(), audioPlugin()] });
|
|
289
|
+
expect(result.ok).toBe(true);
|
|
290
|
+
if (!result.ok) return;
|
|
291
|
+
const app = result.value;
|
|
292
|
+
|
|
293
|
+
// No AudioListener entity spawned — the listener-sync system runs,
|
|
294
|
+
// finds nothing, and silently no-ops. No throw, no crash.
|
|
295
|
+
expect(() => app.start()).not.toThrow();
|
|
296
|
+
|
|
297
|
+
expect(mockCtor).not.toHaveBeenCalled();
|
|
298
|
+
|
|
299
|
+
app.stop();
|
|
300
|
+
} finally {
|
|
301
|
+
uninstallRafStub();
|
|
302
|
+
globalThis.AudioContext = OriginalAudioContext;
|
|
303
|
+
vi.restoreAllMocks();
|
|
304
|
+
vi.unstubAllGlobals();
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('w24 scenario (c): frame-order regression guard — listener pose reads current frame', () => {
|
|
310
|
+
it('modify Transform.posX before second frame → listener reflects new value, not stale', async () => {
|
|
311
|
+
const OriginalAudioContext = globalThis.AudioContext;
|
|
312
|
+
vi.stubGlobal('document', {
|
|
313
|
+
addEventListener: vi.fn(),
|
|
314
|
+
removeEventListener: vi.fn(),
|
|
315
|
+
});
|
|
316
|
+
let context: AudioContext | undefined;
|
|
317
|
+
const mockCtor = vi.fn(function AudioContextMock(this: Record<string, unknown>) {
|
|
318
|
+
context = makeMockAudioContextForListenerSync();
|
|
319
|
+
Object.assign(this, context);
|
|
320
|
+
});
|
|
321
|
+
// biome-ignore lint/suspicious/noExplicitAny: vitest mock for AudioContext constructor -- must use function (not arrow) so new AudioContext() is constructable
|
|
322
|
+
globalThis.AudioContext = mockCtor as any;
|
|
323
|
+
|
|
324
|
+
const rafStub = installRafStub();
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const canvas = {
|
|
328
|
+
tagName: 'canvas',
|
|
329
|
+
isConnected: true,
|
|
330
|
+
width: 800,
|
|
331
|
+
height: 600,
|
|
332
|
+
} as HTMLCanvasElement;
|
|
333
|
+
|
|
334
|
+
const result = await createApp(canvas, { plugins: [webAudioPlugin(), audioPlugin()] });
|
|
335
|
+
expect(result.ok).toBe(true);
|
|
336
|
+
if (!result.ok) return;
|
|
337
|
+
const app = result.value;
|
|
338
|
+
|
|
339
|
+
// Spawn entity at (0,0,0).
|
|
340
|
+
const spawnResult = app.world.spawn(
|
|
341
|
+
{ component: Transform, data: { pos: [0, 0, 0] } },
|
|
342
|
+
{ component: AudioListenerComponent, data: {} },
|
|
343
|
+
);
|
|
344
|
+
expect(spawnResult.ok).toBe(true);
|
|
345
|
+
if (!spawnResult.ok) return;
|
|
346
|
+
const entity = spawnResult.value;
|
|
347
|
+
|
|
348
|
+
// Frame 1: propagateTransforms computes world with posX=0,
|
|
349
|
+
// listener-sync reads it → positionX === 0.
|
|
350
|
+
app.start();
|
|
351
|
+
const listener = context?.listener;
|
|
352
|
+
if (!listener) throw new Error('expected host AudioContext.listener');
|
|
353
|
+
expect(listener.positionX.value).toBe(0);
|
|
354
|
+
|
|
355
|
+
// Modify local position: simulate a frame callback that moves the
|
|
356
|
+
// listener entity. If the listener-sync system ran BEFORE
|
|
357
|
+
// propagateTransforms, the next frame would still read the old
|
|
358
|
+
// world mat4 with posX=0 (1-frame lag).
|
|
359
|
+
const setResult = app.world.set(entity, Transform, { pos: [8, 0, 0] });
|
|
360
|
+
expect(setResult.ok).toBe(true);
|
|
361
|
+
|
|
362
|
+
// Frame 2: propagateTransforms recomputes world with posX=8,
|
|
363
|
+
// listener-sync reads the CURRENT frame's world → positionX === 8.
|
|
364
|
+
// This assertion is the R-listener-frame-order regression guard.
|
|
365
|
+
expect(rafStub.tickFn).not.toBeNull();
|
|
366
|
+
if (rafStub.tickFn) {
|
|
367
|
+
rafStub.tickFn();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
expect(listener.positionX.value).toBe(8);
|
|
371
|
+
|
|
372
|
+
app.stop();
|
|
373
|
+
} finally {
|
|
374
|
+
uninstallRafStub();
|
|
375
|
+
globalThis.AudioContext = OriginalAudioContext;
|
|
376
|
+
vi.restoreAllMocks();
|
|
377
|
+
vi.unstubAllGlobals();
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// create-app-lock-provider.test.ts -- M2 w9: three-hop passthrough real
|
|
2
|
+
// link integration test.
|
|
3
|
+
//
|
|
4
|
+
// Validates that lockProvider flows from CreateAppOptions through
|
|
5
|
+
// InputAttachOptions to BrowserInputBackendOptions without breakage
|
|
6
|
+
// (D-2 three-hop passthrough). Uses a fake canvas + synthetic click
|
|
7
|
+
// event to verify that requestLock is called and sample().pointerLocked
|
|
8
|
+
// reflects the provider lock state.
|
|
9
|
+
//
|
|
10
|
+
// TDD red phase: the lockProvider field on CreateAppOptions /
|
|
11
|
+
// InputAttachOptions is added in w12 (committed), but the full pipeline
|
|
12
|
+
// test validates the end-to-end wiring. This test is green when
|
|
13
|
+
// attachInputAuto correctly forwards lockProvider to the backend.
|
|
14
|
+
//
|
|
15
|
+
// charter awareness:
|
|
16
|
+
// F1 single-entry indexability -- all three-hop tests in one file
|
|
17
|
+
// P3 explicit failure -- each assertion produces a clear diagnostic
|
|
18
|
+
// P4 consistent abstraction -- tests use fake canvas, not real browser PointerLock
|
|
19
|
+
|
|
20
|
+
import { snapshotFromSample } from '@forgeax/engine-input';
|
|
21
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
22
|
+
|
|
23
|
+
import type { AppError } from '../errors';
|
|
24
|
+
import { attachInputAuto, type InputAttachOptions } from '../internal/input-attach';
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Fake DOM infrastructure (mirrors input tests pattern: buildBBFakes).
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
interface FakeTarget {
|
|
31
|
+
addEventListener(kind: string, handler: EventListener): void;
|
|
32
|
+
removeEventListener(kind: string, handler: EventListener): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeFakeDom() {
|
|
36
|
+
// Listener store: Map<target, Map<kind, Set<handler>>>
|
|
37
|
+
const listeners = new Map<string, Map<string, Set<EventListener>>>();
|
|
38
|
+
|
|
39
|
+
function makeTarget(label: string): FakeTarget {
|
|
40
|
+
return {
|
|
41
|
+
addEventListener(kind: string, handler: EventListener): void {
|
|
42
|
+
let perKind = listeners.get(label);
|
|
43
|
+
if (!perKind) {
|
|
44
|
+
perKind = new Map();
|
|
45
|
+
listeners.set(label, perKind);
|
|
46
|
+
}
|
|
47
|
+
let set = perKind.get(kind);
|
|
48
|
+
if (!set) {
|
|
49
|
+
set = new Set();
|
|
50
|
+
perKind.set(kind, set);
|
|
51
|
+
}
|
|
52
|
+
set.add(handler);
|
|
53
|
+
},
|
|
54
|
+
removeEventListener(kind: string, handler: EventListener): void {
|
|
55
|
+
listeners.get(label)?.get(kind)?.delete(handler);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const canvas = {
|
|
61
|
+
...makeTarget('canvas'),
|
|
62
|
+
requestPointerLock: vi.fn(),
|
|
63
|
+
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }) as DOMRect,
|
|
64
|
+
width: 800,
|
|
65
|
+
height: 600,
|
|
66
|
+
style: { touchAction: '' },
|
|
67
|
+
setPointerCapture: vi.fn(),
|
|
68
|
+
} as unknown as HTMLCanvasElement;
|
|
69
|
+
|
|
70
|
+
const doc = {
|
|
71
|
+
...makeTarget('document'),
|
|
72
|
+
hasFocus(): boolean {
|
|
73
|
+
return true;
|
|
74
|
+
},
|
|
75
|
+
visibilityState: 'visible',
|
|
76
|
+
pointerLockElement: null as Element | null,
|
|
77
|
+
exitPointerLock: vi.fn(),
|
|
78
|
+
} as unknown as Document;
|
|
79
|
+
|
|
80
|
+
const win = makeTarget('window') as unknown as Window;
|
|
81
|
+
|
|
82
|
+
function dispatchClick(): void {
|
|
83
|
+
const handlers = listeners.get('canvas')?.get('click');
|
|
84
|
+
if (handlers) {
|
|
85
|
+
const ev = new Event('click', { bubbles: true });
|
|
86
|
+
for (const h of handlers) {
|
|
87
|
+
h(ev as Event);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Simulate W3C pointerlockchange: canvas enters pointer-lock.
|
|
93
|
+
function simulateW3cLock(): void {
|
|
94
|
+
(doc as unknown as { pointerLockElement: Element | null }).pointerLockElement = canvas;
|
|
95
|
+
const handlers = listeners.get('document')?.get('pointerlockchange');
|
|
96
|
+
if (handlers) {
|
|
97
|
+
const ev = new Event('pointerlockchange');
|
|
98
|
+
for (const h of handlers) {
|
|
99
|
+
h(ev as Event);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { canvas, doc, win, dispatchClick, simulateW3cLock, listeners };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Tests
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
describe('create-app-lock-provider (w9)', () => {
|
|
112
|
+
describe('three-hop passthrough: CreateAppOptions -> InputAttachOptions -> BrowserInputBackendOptions', () => {
|
|
113
|
+
it('lockProvider.requestLock is called on canvas click when lockProvider is injected', () => {
|
|
114
|
+
const { canvas, doc, win, dispatchClick } = makeFakeDom();
|
|
115
|
+
|
|
116
|
+
// Stub browser globals (attachBrowserInputBackend reads these).
|
|
117
|
+
vi.stubGlobal('document', doc);
|
|
118
|
+
vi.stubGlobal('window', win);
|
|
119
|
+
vi.stubGlobal('HTMLElement', class {});
|
|
120
|
+
|
|
121
|
+
// Patch performance.now() for gesture debounce (backend checks time
|
|
122
|
+
// since mousedown for trusted-gesture purposes).
|
|
123
|
+
vi.stubGlobal(
|
|
124
|
+
'performance',
|
|
125
|
+
Object.freeze({
|
|
126
|
+
now: () => 0,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const requestLock = vi.fn();
|
|
131
|
+
const exitLock = vi.fn();
|
|
132
|
+
const lockProvider = { requestLock, exitLock };
|
|
133
|
+
|
|
134
|
+
const opts: InputAttachOptions = { lockProvider };
|
|
135
|
+
const handle = attachInputAuto(canvas, opts);
|
|
136
|
+
|
|
137
|
+
// Dispatch a click — the backend's onCanvasClick handler should call
|
|
138
|
+
// lockProvider.requestLock() (provider path takes priority over W3C).
|
|
139
|
+
dispatchClick();
|
|
140
|
+
|
|
141
|
+
expect(requestLock).toHaveBeenCalledOnce();
|
|
142
|
+
|
|
143
|
+
// After provider requestLock succeeds (synchronous return, D-7 optimistic
|
|
144
|
+
// placement), the providerLocked flag should be set, so sample() should
|
|
145
|
+
// report pointerLocked === true.
|
|
146
|
+
const snap = snapshotFromSample(handle.backend.sample());
|
|
147
|
+
expect(snap.mouse.pointerLocked).toBe(true);
|
|
148
|
+
|
|
149
|
+
// Cleanup.
|
|
150
|
+
handle.cleanup();
|
|
151
|
+
|
|
152
|
+
vi.unstubAllGlobals();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('without lockProvider, W3C path is used (pointerlockchange-driven)', () => {
|
|
156
|
+
const { canvas, doc, win, dispatchClick, simulateW3cLock } = makeFakeDom();
|
|
157
|
+
|
|
158
|
+
vi.stubGlobal('document', doc);
|
|
159
|
+
vi.stubGlobal('window', win);
|
|
160
|
+
vi.stubGlobal('HTMLElement', class {});
|
|
161
|
+
vi.stubGlobal(
|
|
162
|
+
'performance',
|
|
163
|
+
Object.freeze({
|
|
164
|
+
now: () => 0,
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const opts: InputAttachOptions = {};
|
|
169
|
+
const handle = attachInputAuto(canvas, opts);
|
|
170
|
+
|
|
171
|
+
// Dispatch a click — the backend should call requestPointerLock on
|
|
172
|
+
// the canvas (W3C path, no provider).
|
|
173
|
+
dispatchClick();
|
|
174
|
+
expect(canvas.requestPointerLock).toHaveBeenCalledOnce();
|
|
175
|
+
|
|
176
|
+
// Simulate W3C pointerlockchange event.
|
|
177
|
+
simulateW3cLock();
|
|
178
|
+
|
|
179
|
+
const snap = snapshotFromSample(handle.backend.sample());
|
|
180
|
+
expect(snap.mouse.pointerLocked).toBe(true);
|
|
181
|
+
|
|
182
|
+
handle.cleanup();
|
|
183
|
+
vi.unstubAllGlobals();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('setOnErrorDispatch receives onLockError when w12 is wired', () => {
|
|
187
|
+
// This tests the onLockError wrapping path. When the backend's
|
|
188
|
+
// onLockError fires (e.g. W3C requestPointerLock rejection), the
|
|
189
|
+
// setOnErrorDispatch callback should be invoked with an AppError
|
|
190
|
+
// tagged 'app-pointer-lock-failed'.
|
|
191
|
+
|
|
192
|
+
const onErrorReceived: AppError[] = [];
|
|
193
|
+
const handle = attachInputAuto({} as HTMLCanvasElement);
|
|
194
|
+
|
|
195
|
+
handle.setOnErrorDispatch((err) => {
|
|
196
|
+
onErrorReceived.push(err);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Call setOnErrorDispatch again — idempotent replacement.
|
|
200
|
+
handle.setOnErrorDispatch((err) => {
|
|
201
|
+
onErrorReceived.push(err);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Verify the setter does not crash and the handle is valid.
|
|
205
|
+
// The actual onLockError firing is covered implicitly when the
|
|
206
|
+
// real backend rejects — but this test validates the wiring seam.
|
|
207
|
+
expect(onErrorReceived).toHaveLength(0);
|
|
208
|
+
|
|
209
|
+
handle.cleanup();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|