@dreamboard-games/sdk 0.4.0-alpha.0 → 0.4.0-alpha.2
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/README.md +8 -1
- package/REFERENCE.md +1849 -379
- package/dist/{ResourceCounter-BTXklCEZ.d.ts → ResourceCounter-BuiNstXs.d.ts} +5 -1
- package/dist/{ThemeProvider-DvMl17Ny.d.ts → ThemeProvider-B1sIoBlX.d.ts} +1 -1
- package/dist/attributes-DbvyMbXw.d.ts +68 -0
- package/dist/authoring/index.d.ts +66 -0
- package/dist/authoring/index.js +560 -0
- package/dist/authoring/index.js.map +1 -0
- package/dist/browser-interaction.d.ts +246 -44
- package/dist/browser-interaction.js +14 -1
- package/dist/{chunk-Q5O7GPVY.js → chunk-45FI6XII.js} +630 -375
- package/dist/chunk-45FI6XII.js.map +1 -0
- package/dist/{chunk-4G7552LO.js → chunk-6JQFGSLD.js} +216 -47
- package/dist/chunk-6JQFGSLD.js.map +1 -0
- package/dist/{chunk-NBAEEHAU.js → chunk-CP35NPPY.js} +5926 -5266
- package/dist/chunk-CP35NPPY.js.map +1 -0
- package/dist/{chunk-2LDZ5C3T.js → chunk-FWVV2X74.js} +2 -2
- package/dist/{chunk-MNKDSGIA.js → chunk-HWJS3VOJ.js} +6 -2
- package/dist/chunk-HWJS3VOJ.js.map +1 -0
- package/dist/chunk-K5ZRJTZZ.js +328 -0
- package/dist/chunk-K5ZRJTZZ.js.map +1 -0
- package/dist/{chunk-U5KBV2BA.js → chunk-N6ZDIMRB.js} +444 -223
- package/dist/chunk-N6ZDIMRB.js.map +1 -0
- package/dist/chunk-O6M6PN4E.js +1225 -0
- package/dist/chunk-O6M6PN4E.js.map +1 -0
- package/dist/chunk-PF7L4BMG.js +117 -0
- package/dist/chunk-PF7L4BMG.js.map +1 -0
- package/dist/{chunk-P7F4L2FF.js → chunk-WOWFONLZ.js} +609 -1287
- package/dist/chunk-WOWFONLZ.js.map +1 -0
- package/dist/{chunk-QLG6VEMW.js → chunk-WUIVY2KE.js} +318 -21
- package/dist/chunk-WUIVY2KE.js.map +1 -0
- package/dist/chunk-ZAS2SENW.js +12 -0
- package/dist/chunk-ZAS2SENW.js.map +1 -0
- package/dist/codegen.d.ts +11 -1
- package/dist/codegen.js +1 -1
- package/dist/{components-DaIqi9QM.d.ts → components-BbWPnZCF.d.ts} +37 -70
- package/dist/{hex-board-view-UCUjMALd.d.ts → hex-board-view-Kz9Q_zsv.d.ts} +1 -1
- package/dist/index.js +2 -1
- package/dist/package-set.d.ts +2 -2
- package/dist/package-set.js +2 -1
- package/dist/player-state-Cqpyeql0.d.ts +371 -0
- package/dist/protocol-BP193lva.d.ts +267 -0
- package/dist/reducer/advanced.d.ts +2 -2
- package/dist/reducer/advanced.js +1 -1
- package/dist/reducer-contract.js +1 -1
- package/dist/reducer.d.ts +7 -7
- package/dist/reducer.js +190 -183
- package/dist/reducer.js.map +1 -1
- package/dist/runtime/primitives.d.ts +10 -9
- package/dist/runtime/primitives.js +9 -6
- package/dist/runtime/runtime-api.d.ts +2 -2
- package/dist/runtime/workspace-contract.d.ts +28 -76
- package/dist/runtime/workspace-contract.js +7 -5
- package/dist/runtime-api-BJharVaj.d.ts +198 -0
- package/dist/runtime-api-C6aWo9c0.d.ts +95 -0
- package/dist/runtime-json-CQ9QbLZ5.d.ts +5 -0
- package/dist/runtime.d.ts +80 -11
- package/dist/runtime.js +89 -445
- package/dist/runtime.js.map +1 -1
- package/dist/testing.d.ts +643 -22
- package/dist/testing.js +1371 -76
- package/dist/testing.js.map +1 -1
- package/dist/types-CYeN0wLK.d.ts +29 -0
- package/dist/{attributes-BeRyboMS.d.ts → types-DJj5MJkl.d.ts} +32 -55
- package/dist/types.d.ts +14 -7
- package/dist/ui/components.d.ts +4 -5
- package/dist/ui/components.js +19 -1
- package/dist/ui/player-state.d.ts +2 -365
- package/dist/ui/plugin-styles.css +1 -1
- package/dist/{ui-contract-BUC6iS3s.d.ts → ui-contract-CY7cxsCC.d.ts} +17 -26
- package/dist/ui.d.ts +8 -13
- package/dist/ui.js +20 -2
- package/dist/{views-B0hlW6IT.d.ts → views-fn2RuXOx.d.ts} +59 -44
- package/package.json +28 -4
- package/dist/chunk-4G7552LO.js.map +0 -1
- package/dist/chunk-MNKDSGIA.js.map +0 -1
- package/dist/chunk-NBAEEHAU.js.map +0 -1
- package/dist/chunk-P7F4L2FF.js.map +0 -1
- package/dist/chunk-Q5O7GPVY.js.map +0 -1
- package/dist/chunk-QLG6VEMW.js.map +0 -1
- package/dist/chunk-U5KBV2BA.js.map +0 -1
- package/dist/hex-color-MhOyuY-o.d.ts +0 -8
- package/dist/runtime-api-BXd70c2e.d.ts +0 -490
- /package/dist/{chunk-2LDZ5C3T.js.map → chunk-FWVV2X74.js.map} +0 -0
package/dist/testing.js
CHANGED
|
@@ -2,6 +2,27 @@ import {
|
|
|
2
2
|
StaleContractArtifactError,
|
|
3
3
|
isStaleContractArtifactError
|
|
4
4
|
} from "./chunk-3OZMHZK3.js";
|
|
5
|
+
import {
|
|
6
|
+
PluginRuntimeBoundary,
|
|
7
|
+
createPluginRuntimeClient
|
|
8
|
+
} from "./chunk-K5ZRJTZZ.js";
|
|
9
|
+
import {
|
|
10
|
+
DREAMBOARD_PLUGIN_PROTOCOL,
|
|
11
|
+
DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
|
|
12
|
+
PluginProtocolFrameSchema,
|
|
13
|
+
PluginProtocolStepSchema,
|
|
14
|
+
PluginProtocolTapeSchema,
|
|
15
|
+
computePluginActionSetVersion,
|
|
16
|
+
materializePluginGameplayFrame
|
|
17
|
+
} from "./chunk-O6M6PN4E.js";
|
|
18
|
+
import {
|
|
19
|
+
BROWSER_INTERACTION_ACTUATOR_KINDS,
|
|
20
|
+
DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION,
|
|
21
|
+
browserInteractionEffectSchema
|
|
22
|
+
} from "./chunk-WUIVY2KE.js";
|
|
23
|
+
import {
|
|
24
|
+
RuntimeJsonSchema
|
|
25
|
+
} from "./chunk-PF7L4BMG.js";
|
|
5
26
|
import "./chunk-PZ5AY32C.js";
|
|
6
27
|
|
|
7
28
|
// src/testing/definitions.ts
|
|
@@ -29,9 +50,9 @@ function matchPartial(actual, expected, path = "value") {
|
|
|
29
50
|
if (!(key in actual)) {
|
|
30
51
|
return `${path}.${key} is missing`;
|
|
31
52
|
}
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
return
|
|
53
|
+
const mismatch2 = matchPartial(actualValue, expectedValue, `${path}.${key}`);
|
|
54
|
+
if (mismatch2) {
|
|
55
|
+
return mismatch2;
|
|
35
56
|
}
|
|
36
57
|
}
|
|
37
58
|
return null;
|
|
@@ -51,9 +72,9 @@ function assertDescriptorMatches(descriptor, opts) {
|
|
|
51
72
|
if (!opts) {
|
|
52
73
|
return;
|
|
53
74
|
}
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
throw new Error(
|
|
75
|
+
const mismatch2 = matchPartial(descriptor, opts, "interaction");
|
|
76
|
+
if (mismatch2) {
|
|
77
|
+
throw new Error(mismatch2);
|
|
57
78
|
}
|
|
58
79
|
}
|
|
59
80
|
function createSubmissionErrorMatcher(actual, expected, options) {
|
|
@@ -170,9 +191,9 @@ function createExpectApi(options = {}) {
|
|
|
170
191
|
}
|
|
171
192
|
},
|
|
172
193
|
toMatchObject: (expected) => {
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
throw new Error(
|
|
194
|
+
const mismatch2 = matchPartial(actual, expected);
|
|
195
|
+
if (mismatch2) {
|
|
196
|
+
throw new Error(mismatch2);
|
|
176
197
|
}
|
|
177
198
|
},
|
|
178
199
|
toBeDefined: () => {
|
|
@@ -414,7 +435,7 @@ function resolvePlayerIds(options) {
|
|
|
414
435
|
function isWireZoneHandles(value) {
|
|
415
436
|
return typeof value === "object" && value !== null;
|
|
416
437
|
}
|
|
417
|
-
function
|
|
438
|
+
function buildPluginFrame(options) {
|
|
418
439
|
const projection = options.bundle.projectSeatsDynamic({
|
|
419
440
|
state: options.state,
|
|
420
441
|
playerIds: [options.playerId]
|
|
@@ -449,28 +470,25 @@ function buildPluginSnapshot(options) {
|
|
|
449
470
|
];
|
|
450
471
|
})
|
|
451
472
|
);
|
|
473
|
+
const availableInteractions = hydrateRefs(
|
|
474
|
+
seat?.availableInteractionRefs
|
|
475
|
+
);
|
|
452
476
|
return {
|
|
477
|
+
gameVersion: options.version,
|
|
478
|
+
actionSetVersion: computePluginActionSetVersion({
|
|
479
|
+
gameVersion: options.version,
|
|
480
|
+
availableInteractions
|
|
481
|
+
}),
|
|
482
|
+
perspectivePlayerId: options.playerId || null,
|
|
453
483
|
view: seat?.view ?? null,
|
|
454
|
-
|
|
484
|
+
flow: {
|
|
455
485
|
currentPhase: flow.currentPhase,
|
|
456
486
|
currentStage: projection.currentStage ?? null,
|
|
457
487
|
activePlayers: flow.activePlayers,
|
|
458
|
-
simultaneousPhase: projection.simultaneousPhase ?? null
|
|
459
|
-
availableInteractions: hydrateRefs(
|
|
460
|
-
seat?.availableInteractionRefs
|
|
461
|
-
),
|
|
462
|
-
zones
|
|
488
|
+
simultaneousPhase: projection.simultaneousPhase ?? null
|
|
463
489
|
},
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
session: {
|
|
467
|
-
sessionId: options.sessionId,
|
|
468
|
-
controllablePlayerIds: [...options.playerIds],
|
|
469
|
-
controllingPlayerId: options.playerId || null,
|
|
470
|
-
userId: options.userId
|
|
471
|
-
},
|
|
472
|
-
history: null,
|
|
473
|
-
syncId: options.version
|
|
490
|
+
availableInteractions,
|
|
491
|
+
zones
|
|
474
492
|
};
|
|
475
493
|
}
|
|
476
494
|
function createTestRuntime(options) {
|
|
@@ -491,44 +509,37 @@ function createTestRuntime(options) {
|
|
|
491
509
|
baseState,
|
|
492
510
|
explicitPlayerIds: options.playerIds
|
|
493
511
|
});
|
|
494
|
-
const userId = options.userId ?? "test-user";
|
|
495
512
|
const sessionId = options.sessionId ?? "test-session";
|
|
513
|
+
const sessionDescriptor = {
|
|
514
|
+
sessionId,
|
|
515
|
+
players: playerIds.map((playerId) => ({
|
|
516
|
+
playerId,
|
|
517
|
+
displayName: options.displayNameByPlayerId?.[playerId] ?? playerId
|
|
518
|
+
}))
|
|
519
|
+
};
|
|
496
520
|
let version = 0;
|
|
497
521
|
let submissionCounter = 0;
|
|
498
522
|
let currentPlayerId = playerIds[0] ?? "";
|
|
499
523
|
const diagnosticEvents = [];
|
|
500
524
|
let lastDispatch = null;
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
status: "ready"
|
|
525
|
+
const toReadySessionState = (frame) => ({
|
|
526
|
+
status: "ready",
|
|
527
|
+
sessionId,
|
|
528
|
+
controllingPlayerId: frame.perspectivePlayerId
|
|
506
529
|
});
|
|
507
|
-
let
|
|
530
|
+
let lastPluginFrame;
|
|
508
531
|
let lastSessionState;
|
|
509
532
|
const applyCurrentState = () => {
|
|
510
|
-
const previousSession = lastSessionState;
|
|
511
533
|
version += 1;
|
|
512
|
-
|
|
534
|
+
lastPluginFrame = buildPluginFrame({
|
|
513
535
|
state: currentState,
|
|
514
536
|
bundle: options.bundle,
|
|
515
537
|
playerId: currentPlayerId,
|
|
516
|
-
playerIds,
|
|
517
|
-
userId,
|
|
518
|
-
sessionId,
|
|
519
538
|
version,
|
|
520
539
|
expectedPhase: version === 1 ? options.phase : void 0,
|
|
521
540
|
baseId: options.baseId
|
|
522
541
|
});
|
|
523
|
-
lastSessionState = toReadySessionState(
|
|
524
|
-
for (const listener of stateListeners) {
|
|
525
|
-
listener(lastPluginSnapshot);
|
|
526
|
-
}
|
|
527
|
-
if (!previousSession || previousSession.controllingPlayerId !== lastSessionState.controllingPlayerId || previousSession.controllablePlayerIds.join("\0") !== lastSessionState.controllablePlayerIds.join("\0")) {
|
|
528
|
-
for (const listener of sessionListeners) {
|
|
529
|
-
listener(lastSessionState);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
542
|
+
lastSessionState = toReadySessionState(lastPluginFrame);
|
|
532
543
|
};
|
|
533
544
|
applyCurrentState();
|
|
534
545
|
const validate = async (playerId, interactionId, params = {}) => {
|
|
@@ -607,38 +618,16 @@ function createTestRuntime(options) {
|
|
|
607
618
|
interactionId
|
|
608
619
|
});
|
|
609
620
|
};
|
|
610
|
-
const setControllingPlayer = (playerId) => {
|
|
611
|
-
if (!playerIds.includes(playerId)) {
|
|
612
|
-
throw new Error(`Unknown controlling player '${playerId}'.`);
|
|
613
|
-
}
|
|
614
|
-
currentPlayerId = playerId;
|
|
615
|
-
applyCurrentState();
|
|
616
|
-
};
|
|
617
621
|
const runtime = {
|
|
618
|
-
validateInteraction: validate,
|
|
619
|
-
submitInteraction: submit,
|
|
622
|
+
validateInteraction: (interactionId, params) => validate(currentPlayerId, interactionId, params),
|
|
623
|
+
submitInteraction: (interactionId, params) => submit(currentPlayerId, interactionId, params),
|
|
620
624
|
getSessionState: () => lastSessionState,
|
|
621
|
-
disconnect: () => void 0
|
|
622
|
-
switchPlayer: (playerId) => {
|
|
623
|
-
setControllingPlayer(playerId);
|
|
624
|
-
},
|
|
625
|
-
getSnapshot: () => lastPluginSnapshot,
|
|
626
|
-
subscribeToState: (listener) => {
|
|
627
|
-
stateListeners.add(listener);
|
|
628
|
-
return () => {
|
|
629
|
-
stateListeners.delete(listener);
|
|
630
|
-
};
|
|
631
|
-
},
|
|
632
|
-
_subscribeToSessionState: (listener) => {
|
|
633
|
-
sessionListeners.add(listener);
|
|
634
|
-
return () => {
|
|
635
|
-
sessionListeners.delete(listener);
|
|
636
|
-
};
|
|
637
|
-
}
|
|
625
|
+
disconnect: () => void 0
|
|
638
626
|
};
|
|
639
627
|
return {
|
|
640
628
|
runtime,
|
|
641
|
-
|
|
629
|
+
getFrame: () => lastPluginFrame,
|
|
630
|
+
getSessionDescriptor: () => sessionDescriptor,
|
|
642
631
|
players: () => [...playerIds],
|
|
643
632
|
seat: (index) => {
|
|
644
633
|
if (!Number.isInteger(index) || index < 0 || index >= playerIds.length) {
|
|
@@ -662,16 +651,1322 @@ function createTestRuntime(options) {
|
|
|
662
651
|
diagnosticEvents.length = 0;
|
|
663
652
|
lastDispatch = null;
|
|
664
653
|
}
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/testing/ui-fixture/canonical.ts
|
|
659
|
+
import { createHash } from "crypto";
|
|
660
|
+
var sha256DigestPattern = /^sha256:[a-f0-9]{64}$/;
|
|
661
|
+
function isSha256Digest(value) {
|
|
662
|
+
return sha256DigestPattern.test(value);
|
|
663
|
+
}
|
|
664
|
+
function canonicalizeUIFixtureJson(value) {
|
|
665
|
+
if (value === null) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
switch (typeof value) {
|
|
669
|
+
case "boolean":
|
|
670
|
+
case "string":
|
|
671
|
+
return value;
|
|
672
|
+
case "number":
|
|
673
|
+
if (Number.isFinite(value)) {
|
|
674
|
+
return value;
|
|
675
|
+
}
|
|
676
|
+
break;
|
|
677
|
+
case "object": {
|
|
678
|
+
if (Array.isArray(value)) {
|
|
679
|
+
return value.map((item) => canonicalizeUIFixtureJson(item));
|
|
680
|
+
}
|
|
681
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) {
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
const entries = Object.entries(value);
|
|
685
|
+
return Object.fromEntries(
|
|
686
|
+
entries.filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalizeUIFixtureJson(item)])
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
throw new Error(
|
|
691
|
+
`UI fixture values must be deterministic JSON. Received ${typeof value}.`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
function canonicalUIFixtureJson(value) {
|
|
695
|
+
return JSON.stringify(canonicalizeUIFixtureJson(value));
|
|
696
|
+
}
|
|
697
|
+
function digestUIFixtureJson(value) {
|
|
698
|
+
return sha256(canonicalUIFixtureJson(value));
|
|
699
|
+
}
|
|
700
|
+
function digestUIFixtureRequest(request) {
|
|
701
|
+
return digestUIFixtureJson({
|
|
702
|
+
digestVersion: "ui-replay-request@2",
|
|
703
|
+
request
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
function digestUIFixtureTransportRequest(request) {
|
|
707
|
+
return digestUIFixtureJson({
|
|
708
|
+
digestVersion: "ui-fixture-runtime-command@2",
|
|
709
|
+
request
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
function digestPluginProtocolTape(tape) {
|
|
713
|
+
return digestUIFixtureJson({
|
|
714
|
+
digestVersion: "plugin-protocol-tape@2",
|
|
715
|
+
tape
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
function canonicalizeUIScenarioFixture(fixture) {
|
|
719
|
+
return canonicalizeUIFixtureJson({
|
|
720
|
+
...fixture,
|
|
721
|
+
tags: [...fixture.tags].sort(),
|
|
722
|
+
environment: {
|
|
723
|
+
...fixture.environment,
|
|
724
|
+
viewportTags: [...fixture.environment.viewportTags].sort()
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
function serializeUIScenarioFixture(fixture) {
|
|
729
|
+
return `${JSON.stringify(canonicalizeUIScenarioFixture(fixture), null, 2)}
|
|
730
|
+
`;
|
|
731
|
+
}
|
|
732
|
+
function digestUIScenarioFixture(fixture) {
|
|
733
|
+
return sha256(serializeUIScenarioFixture(fixture));
|
|
734
|
+
}
|
|
735
|
+
function sha256(value) {
|
|
736
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/testing/reducer-scenario/create-reducer-scenario-runner.ts
|
|
740
|
+
function createReducerScenarioRunner(options) {
|
|
741
|
+
return {
|
|
742
|
+
async run(operations) {
|
|
743
|
+
let state = structuredClone(options.initialState);
|
|
744
|
+
let gameVersion = options.initialGameVersion ?? 1;
|
|
745
|
+
const frames = [];
|
|
746
|
+
const exchanges = [];
|
|
747
|
+
const appendFrame = (id) => {
|
|
748
|
+
const dynamicProjection = options.bundle.projectSeatsDynamic({
|
|
749
|
+
state,
|
|
750
|
+
playerIds: [options.viewer.playerId]
|
|
751
|
+
});
|
|
752
|
+
const staticProjection = options.bundle.projectStatic?.() ?? null;
|
|
753
|
+
const flow = readFlowState2(state);
|
|
754
|
+
const materialized = materializePluginGameplayFrame({
|
|
755
|
+
currentPhase: flow.currentPhase,
|
|
756
|
+
activePlayers: flow.activePlayers,
|
|
757
|
+
dynamicProjection,
|
|
758
|
+
staticProjection,
|
|
759
|
+
perspectivePlayerId: options.viewer.playerId,
|
|
760
|
+
gameVersion,
|
|
761
|
+
actionSetVersion: "pending"
|
|
762
|
+
});
|
|
763
|
+
const actionSetVersion = computePluginActionSetVersion({
|
|
764
|
+
gameVersion,
|
|
765
|
+
availableInteractions: materialized.availableInteractions
|
|
766
|
+
});
|
|
767
|
+
const frame = {
|
|
768
|
+
id,
|
|
769
|
+
reducerState: structuredClone(state),
|
|
770
|
+
dynamicProjection,
|
|
771
|
+
staticProjection,
|
|
772
|
+
gameVersion,
|
|
773
|
+
actionSetVersion,
|
|
774
|
+
projectionDigest: digestUIFixtureJson({
|
|
775
|
+
digestVersion: "reducer-scenario-frame@2",
|
|
776
|
+
scenarioId: options.scenarioId,
|
|
777
|
+
frameId: id,
|
|
778
|
+
dynamicProjection,
|
|
779
|
+
staticProjection,
|
|
780
|
+
gameVersion,
|
|
781
|
+
actionSetVersion
|
|
782
|
+
})
|
|
783
|
+
};
|
|
784
|
+
frames.push(frame);
|
|
785
|
+
return frame;
|
|
786
|
+
};
|
|
787
|
+
appendFrame("frame-1");
|
|
788
|
+
for (const operation of operations) {
|
|
789
|
+
const fromFrame = frames[frames.length - 1];
|
|
790
|
+
if (!fromFrame) {
|
|
791
|
+
throw new Error("Reducer scenario runner lost its initial frame.");
|
|
792
|
+
}
|
|
793
|
+
if (operation.operation === "validate") {
|
|
794
|
+
const result2 = await options.bundle.validateInput({
|
|
795
|
+
state,
|
|
796
|
+
input: operation.input
|
|
797
|
+
});
|
|
798
|
+
exchanges.push({
|
|
799
|
+
id: operation.id,
|
|
800
|
+
operation: "validate",
|
|
801
|
+
fromFrameId: fromFrame.id,
|
|
802
|
+
input: operation.input,
|
|
803
|
+
result: result2
|
|
804
|
+
});
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const result = await options.bundle.dispatch({
|
|
808
|
+
state,
|
|
809
|
+
input: operation.input
|
|
810
|
+
});
|
|
811
|
+
if (result.kind === "reject") {
|
|
812
|
+
exchanges.push({
|
|
813
|
+
id: operation.id,
|
|
814
|
+
operation: "submit",
|
|
815
|
+
fromFrameId: fromFrame.id,
|
|
816
|
+
input: operation.input,
|
|
817
|
+
result: {
|
|
818
|
+
kind: "rejected",
|
|
819
|
+
diagnostics: [
|
|
820
|
+
{
|
|
821
|
+
code: result.errorCode,
|
|
822
|
+
message: result.message ?? result.errorCode
|
|
823
|
+
}
|
|
824
|
+
]
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
state = structuredClone(result.state);
|
|
830
|
+
gameVersion += 1;
|
|
831
|
+
const toFrame = appendFrame(`frame-${frames.length + 1}`);
|
|
832
|
+
exchanges.push({
|
|
833
|
+
id: operation.id,
|
|
834
|
+
operation: "submit",
|
|
835
|
+
fromFrameId: fromFrame.id,
|
|
836
|
+
input: operation.input,
|
|
837
|
+
result: {
|
|
838
|
+
kind: "accepted",
|
|
839
|
+
toFrameId: toFrame.id
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
scenarioId: options.scenarioId,
|
|
845
|
+
gameId: options.gameId,
|
|
846
|
+
viewer: options.viewer,
|
|
847
|
+
frames,
|
|
848
|
+
exchanges
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
function readFlowState2(state) {
|
|
854
|
+
return {
|
|
855
|
+
currentPhase: state.domain?.flow?.currentPhase ?? null,
|
|
856
|
+
activePlayers: [...state.domain?.flow?.activePlayers ?? []]
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/testing/ui-fixture/parity.ts
|
|
861
|
+
var topLevelComparisons = [
|
|
862
|
+
{ path: "scenarioId", code: "fixture-source-mismatch" },
|
|
863
|
+
{ path: "fixtureDigest", code: "candidate-mismatch" },
|
|
864
|
+
{ path: "sdkCandidateDigest", code: "candidate-mismatch" },
|
|
865
|
+
{ path: "pluginRuntimeProtocol", code: "protocol-mismatch" },
|
|
866
|
+
{ path: "browserInteractionProtocol", code: "protocol-mismatch" },
|
|
867
|
+
{ path: "environment.project", code: "host-setup-failure" },
|
|
868
|
+
{ path: "environment.viewport.width", code: "host-setup-failure" },
|
|
869
|
+
{ path: "environment.viewport.height", code: "host-setup-failure" }
|
|
870
|
+
];
|
|
871
|
+
var checkpointComparisons = [
|
|
872
|
+
{ path: "stepId", code: "interaction-resolution-mismatch" },
|
|
873
|
+
{ path: "interactionKey", code: "interaction-resolution-mismatch" },
|
|
874
|
+
{ path: "interactionId", code: "interaction-resolution-mismatch" },
|
|
875
|
+
{ path: "actuatorId", code: "interaction-resolution-mismatch" },
|
|
876
|
+
{ path: "descriptorDigest", code: "interaction-resolution-mismatch" },
|
|
877
|
+
{ path: "gameVersion", code: "projection-mismatch" },
|
|
878
|
+
{ path: "actionSetVersion", code: "projection-mismatch" },
|
|
879
|
+
{ path: "perspectivePlayerId", code: "projection-mismatch" },
|
|
880
|
+
{ path: "projectionDigest", code: "projection-mismatch" },
|
|
881
|
+
{ path: "semanticDigest", code: "semantic-snapshot-mismatch" },
|
|
882
|
+
{ path: "draftDigest", code: "draft-mismatch" },
|
|
883
|
+
{ path: "submissionDigest", code: "submission-mismatch" },
|
|
884
|
+
{ path: "screenshot", code: "visual-mismatch" }
|
|
885
|
+
];
|
|
886
|
+
function createUIParityObservationFromFixture({
|
|
887
|
+
fixture,
|
|
888
|
+
sdkCandidateDigest,
|
|
889
|
+
environment,
|
|
890
|
+
fixtureDigest = digestUIScenarioFixture(fixture),
|
|
891
|
+
screenshotsByStepId = {},
|
|
892
|
+
diagnostics = []
|
|
893
|
+
}) {
|
|
894
|
+
const frameById = new Map(
|
|
895
|
+
fixture.protocol.frames.map((frame) => [frame.id, frame])
|
|
896
|
+
);
|
|
897
|
+
const firstFrame = fixture.protocol.frames[0];
|
|
898
|
+
if (!firstFrame) {
|
|
899
|
+
throw new Error(
|
|
900
|
+
`Fixture '${fixture.id}' does not contain protocol frames.`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
const checkpoints = fixture.replay.map((step, index) => {
|
|
904
|
+
const expectedFrameId = step.expect.frameId ?? firstFrame.id;
|
|
905
|
+
const frame = frameById.get(expectedFrameId);
|
|
906
|
+
if (!frame) {
|
|
907
|
+
throw new Error(
|
|
908
|
+
`Fixture '${fixture.id}' replay step '${step.stepId}' references missing frame '${expectedFrameId}'.`
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
const identity = "expectedIdentity" in step ? step.expectedIdentity : null;
|
|
912
|
+
const projectionDigest = step.expect.projectionDigest ?? frame.projectionDigest;
|
|
913
|
+
const isFinal = index === fixture.replay.length - 1;
|
|
914
|
+
const semanticDigest = step.expect.semanticDigest ?? (isFinal ? fixture.expected.finalSemanticDigest : digestUIFixtureJson({
|
|
915
|
+
scenarioId: fixture.id,
|
|
916
|
+
stepId: step.stepId,
|
|
917
|
+
frameId: frame.id,
|
|
918
|
+
projectionDigest
|
|
919
|
+
}));
|
|
920
|
+
const submissionDigest = step.expect.submissionDigest ?? (isFinal ? fixture.expected.submissionDigest : void 0);
|
|
921
|
+
return {
|
|
922
|
+
stepId: step.stepId,
|
|
923
|
+
interactionKey: identity?.interactionKey,
|
|
924
|
+
interactionId: identity?.interactionId,
|
|
925
|
+
actuatorId: identity?.actuatorId,
|
|
926
|
+
descriptorDigest: identity?.descriptorDigest,
|
|
927
|
+
draftDigest: step.expect.draftDigest ?? identity?.draftDigest,
|
|
928
|
+
gameVersion: frame.frame.gameVersion,
|
|
929
|
+
actionSetVersion: frame.frame.actionSetVersion,
|
|
930
|
+
perspectivePlayerId: frame.frame.perspectivePlayerId,
|
|
931
|
+
projectionDigest,
|
|
932
|
+
semanticDigest,
|
|
933
|
+
submissionDigest,
|
|
934
|
+
screenshot: screenshotsByStepId[step.stepId]
|
|
935
|
+
};
|
|
936
|
+
});
|
|
937
|
+
return {
|
|
938
|
+
schemaVersion: 1,
|
|
939
|
+
scenarioId: fixture.id,
|
|
940
|
+
fixtureDigest,
|
|
941
|
+
sdkCandidateDigest,
|
|
942
|
+
pluginRuntimeProtocol: fixture.pluginRuntimeProtocol,
|
|
943
|
+
browserInteractionProtocol: fixture.browserInteractionProtocol,
|
|
944
|
+
environment,
|
|
945
|
+
checkpoints,
|
|
946
|
+
diagnostics
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function parseUIParityObservationV1(value) {
|
|
950
|
+
const observation = assertRecord(value, "observation");
|
|
951
|
+
if (observation.schemaVersion !== 1) {
|
|
952
|
+
throw new Error("UI parity observation schemaVersion must be 1.");
|
|
953
|
+
}
|
|
954
|
+
assertString(observation.scenarioId, "scenarioId");
|
|
955
|
+
assertString(observation.fixtureDigest, "fixtureDigest");
|
|
956
|
+
assertString(observation.sdkCandidateDigest, "sdkCandidateDigest");
|
|
957
|
+
if (observation.pluginRuntimeProtocol !== 3) {
|
|
958
|
+
throw new Error("UI parity observation pluginRuntimeProtocol must be 3.");
|
|
959
|
+
}
|
|
960
|
+
assertString(
|
|
961
|
+
observation.browserInteractionProtocol,
|
|
962
|
+
"browserInteractionProtocol"
|
|
963
|
+
);
|
|
964
|
+
const environment = assertRecord(observation.environment, "environment");
|
|
965
|
+
assertString(environment.project, "environment.project");
|
|
966
|
+
const viewport = assertRecord(environment.viewport, "environment.viewport");
|
|
967
|
+
assertNumber(viewport.width, "environment.viewport.width");
|
|
968
|
+
assertNumber(viewport.height, "environment.viewport.height");
|
|
969
|
+
const checkpoints = assertArray(observation.checkpoints, "checkpoints");
|
|
970
|
+
for (let index = 0; index < checkpoints.length; index += 1) {
|
|
971
|
+
const checkpoint = assertRecord(
|
|
972
|
+
checkpoints[index],
|
|
973
|
+
`checkpoints[${index}]`
|
|
974
|
+
);
|
|
975
|
+
assertString(checkpoint.stepId, `checkpoints[${index}].stepId`);
|
|
976
|
+
assertOptionalString(
|
|
977
|
+
checkpoint.interactionKey,
|
|
978
|
+
`checkpoints[${index}].interactionKey`
|
|
979
|
+
);
|
|
980
|
+
assertOptionalString(
|
|
981
|
+
checkpoint.interactionId,
|
|
982
|
+
`checkpoints[${index}].interactionId`
|
|
983
|
+
);
|
|
984
|
+
assertOptionalString(
|
|
985
|
+
checkpoint.actuatorId,
|
|
986
|
+
`checkpoints[${index}].actuatorId`
|
|
987
|
+
);
|
|
988
|
+
assertOptionalString(
|
|
989
|
+
checkpoint.descriptorDigest,
|
|
990
|
+
`checkpoints[${index}].descriptorDigest`
|
|
991
|
+
);
|
|
992
|
+
assertOptionalString(
|
|
993
|
+
checkpoint.draftDigest,
|
|
994
|
+
`checkpoints[${index}].draftDigest`
|
|
995
|
+
);
|
|
996
|
+
assertNumber(checkpoint.gameVersion, `checkpoints[${index}].gameVersion`);
|
|
997
|
+
assertString(
|
|
998
|
+
checkpoint.actionSetVersion,
|
|
999
|
+
`checkpoints[${index}].actionSetVersion`
|
|
1000
|
+
);
|
|
1001
|
+
if (checkpoint.perspectivePlayerId !== null && typeof checkpoint.perspectivePlayerId !== "string") {
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
`checkpoints[${index}].perspectivePlayerId must be a string or null.`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
assertString(
|
|
1007
|
+
checkpoint.projectionDigest,
|
|
1008
|
+
`checkpoints[${index}].projectionDigest`
|
|
1009
|
+
);
|
|
1010
|
+
assertString(
|
|
1011
|
+
checkpoint.semanticDigest,
|
|
1012
|
+
`checkpoints[${index}].semanticDigest`
|
|
1013
|
+
);
|
|
1014
|
+
assertOptionalString(
|
|
1015
|
+
checkpoint.submissionDigest,
|
|
1016
|
+
`checkpoints[${index}].submissionDigest`
|
|
1017
|
+
);
|
|
1018
|
+
assertOptionalString(
|
|
1019
|
+
checkpoint.screenshot,
|
|
1020
|
+
`checkpoints[${index}].screenshot`
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
const diagnostics = assertArray(observation.diagnostics, "diagnostics");
|
|
1024
|
+
for (let index = 0; index < diagnostics.length; index += 1) {
|
|
1025
|
+
const diagnostic = assertRecord(
|
|
1026
|
+
diagnostics[index],
|
|
1027
|
+
`diagnostics[${index}]`
|
|
1028
|
+
);
|
|
1029
|
+
assertString(diagnostic.code, `diagnostics[${index}].code`);
|
|
1030
|
+
assertString(diagnostic.message, `diagnostics[${index}].message`);
|
|
1031
|
+
}
|
|
1032
|
+
return observation;
|
|
1033
|
+
}
|
|
1034
|
+
function compareUIParityObservations(expected, actual) {
|
|
1035
|
+
for (const comparison of topLevelComparisons) {
|
|
1036
|
+
const result = comparePath(expected, actual, comparison);
|
|
1037
|
+
if (result) {
|
|
1038
|
+
return result;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (expected.checkpoints.length !== actual.checkpoints.length) {
|
|
1042
|
+
return mismatch({
|
|
1043
|
+
code: "interaction-resolution-mismatch",
|
|
1044
|
+
path: "checkpoints.length",
|
|
1045
|
+
expected: expected.checkpoints.length,
|
|
1046
|
+
actual: actual.checkpoints.length
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
for (let index = 0; index < expected.checkpoints.length; index += 1) {
|
|
1050
|
+
const expectedCheckpoint = expected.checkpoints[index];
|
|
1051
|
+
const actualCheckpoint = actual.checkpoints[index];
|
|
1052
|
+
for (const comparison of checkpointComparisons) {
|
|
1053
|
+
const result = comparePath(
|
|
1054
|
+
expectedCheckpoint,
|
|
1055
|
+
actualCheckpoint,
|
|
1056
|
+
comparison,
|
|
1057
|
+
{
|
|
1058
|
+
prefix: `checkpoints[${index}]`,
|
|
1059
|
+
checkpointIndex: index,
|
|
1060
|
+
stepId: expectedCheckpoint.stepId
|
|
1061
|
+
}
|
|
1062
|
+
);
|
|
1063
|
+
if (result) {
|
|
1064
|
+
return result;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (!sameValue(expected.diagnostics, actual.diagnostics)) {
|
|
1069
|
+
return mismatch({
|
|
1070
|
+
code: "host-setup-failure",
|
|
1071
|
+
path: "diagnostics",
|
|
1072
|
+
expected: expected.diagnostics,
|
|
1073
|
+
actual: actual.diagnostics
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
return { ok: true };
|
|
1077
|
+
}
|
|
1078
|
+
function comparePath(expected, actual, comparison, options = {}) {
|
|
1079
|
+
const expectedValue = getPath(expected, comparison.path);
|
|
1080
|
+
const actualValue = getPath(actual, comparison.path);
|
|
1081
|
+
if (sameValue(expectedValue, actualValue)) {
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
const path = options.prefix ? `${options.prefix}.${comparison.path}` : comparison.path;
|
|
1085
|
+
return mismatch({
|
|
1086
|
+
code: comparison.code,
|
|
1087
|
+
path,
|
|
1088
|
+
expected: expectedValue,
|
|
1089
|
+
actual: actualValue,
|
|
1090
|
+
checkpointIndex: options.checkpointIndex,
|
|
1091
|
+
stepId: options.stepId
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
function mismatch(input) {
|
|
1095
|
+
return {
|
|
1096
|
+
ok: false,
|
|
1097
|
+
failure: {
|
|
1098
|
+
...input,
|
|
1099
|
+
message: `${input.code} at ${input.path}`
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function sameValue(left, right) {
|
|
1104
|
+
if (left === void 0 || right === void 0) {
|
|
1105
|
+
return left === right;
|
|
1106
|
+
}
|
|
1107
|
+
return canonicalUIFixtureJson(left) === canonicalUIFixtureJson(right);
|
|
1108
|
+
}
|
|
1109
|
+
function getPath(value, path) {
|
|
1110
|
+
return path.split(".").reduce((current, key) => {
|
|
1111
|
+
if (current === null || typeof current !== "object") {
|
|
1112
|
+
return void 0;
|
|
1113
|
+
}
|
|
1114
|
+
return current[key];
|
|
1115
|
+
}, value);
|
|
1116
|
+
}
|
|
1117
|
+
function assertRecord(value, path) {
|
|
1118
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1119
|
+
throw new Error(`${path} must be an object.`);
|
|
1120
|
+
}
|
|
1121
|
+
return value;
|
|
1122
|
+
}
|
|
1123
|
+
function assertArray(value, path) {
|
|
1124
|
+
if (!Array.isArray(value)) {
|
|
1125
|
+
throw new Error(`${path} must be an array.`);
|
|
1126
|
+
}
|
|
1127
|
+
return value;
|
|
1128
|
+
}
|
|
1129
|
+
function assertString(value, path) {
|
|
1130
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1131
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function assertOptionalString(value, path) {
|
|
1135
|
+
if (value !== void 0 && (typeof value !== "string" || value.length === 0)) {
|
|
1136
|
+
throw new Error(`${path} must be a non-empty string when present.`);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
function assertNumber(value, path) {
|
|
1140
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1141
|
+
throw new Error(`${path} must be a finite number.`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/testing/ui-fixture/create-fixture-host-harness.ts
|
|
1146
|
+
var DEFAULT_CHANNEL_ID = "fixture-channel";
|
|
1147
|
+
function createFixtureHostHarness(options) {
|
|
1148
|
+
const strict = options.strict ?? true;
|
|
1149
|
+
const latencyMs = options.latencyMs ?? 0;
|
|
1150
|
+
const channelId = options.channelId ?? DEFAULT_CHANNEL_ID;
|
|
1151
|
+
const events = [];
|
|
1152
|
+
const frameById = new Map(
|
|
1153
|
+
options.tape.frames.map((frame) => [frame.id, frame])
|
|
1154
|
+
);
|
|
1155
|
+
let eventSequence = 0;
|
|
1156
|
+
let envelopeSequence = 0;
|
|
1157
|
+
let stepCursor = 0;
|
|
1158
|
+
let currentFrameId = null;
|
|
1159
|
+
let onMessage = null;
|
|
1160
|
+
let firstFixtureError = null;
|
|
1161
|
+
const pendingHandlers = /* @__PURE__ */ new Set();
|
|
1162
|
+
const record = (event) => {
|
|
1163
|
+
const entry = {
|
|
1164
|
+
sequence: ++eventSequence,
|
|
1165
|
+
atMs: options.nowMs?.() ?? eventSequence,
|
|
1166
|
+
...event
|
|
1167
|
+
};
|
|
1168
|
+
events.push(entry);
|
|
1169
|
+
options.onEvent?.(entry);
|
|
1170
|
+
};
|
|
1171
|
+
const createFixtureError = (code, message) => {
|
|
1172
|
+
record({ kind: "diagnostic" });
|
|
1173
|
+
const error = new Error(message);
|
|
1174
|
+
error.name = "FixtureHostHarnessError";
|
|
1175
|
+
Object.assign(error, { errorCode: code });
|
|
1176
|
+
return error;
|
|
1177
|
+
};
|
|
1178
|
+
const rememberFixtureError = (error) => {
|
|
1179
|
+
if (firstFixtureError) {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
firstFixtureError = error instanceof Error ? error : createFixtureError(
|
|
1183
|
+
"fixture-unknown-error",
|
|
1184
|
+
"Fixture host failed with an unknown error."
|
|
1185
|
+
);
|
|
1186
|
+
};
|
|
1187
|
+
const throwFixtureError = () => {
|
|
1188
|
+
if (firstFixtureError) {
|
|
1189
|
+
throw firstFixtureError;
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
const sendHostPayload = (payload) => {
|
|
1193
|
+
if (!onMessage) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
onMessage({
|
|
1197
|
+
protocol: DREAMBOARD_PLUGIN_PROTOCOL,
|
|
1198
|
+
version: DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
|
|
1199
|
+
channelId,
|
|
1200
|
+
sequence: ++envelopeSequence,
|
|
1201
|
+
payload
|
|
1202
|
+
});
|
|
1203
|
+
};
|
|
1204
|
+
const sendInit = () => {
|
|
1205
|
+
sendHostPayload({
|
|
1206
|
+
type: "runtime.init",
|
|
1207
|
+
session: options.tape.session
|
|
1208
|
+
});
|
|
1209
|
+
};
|
|
1210
|
+
const publishFrame = (frameId) => {
|
|
1211
|
+
const frame = frameById.get(frameId);
|
|
1212
|
+
if (!frame) {
|
|
1213
|
+
throw createFixtureError(
|
|
1214
|
+
"fixture-missing-frame",
|
|
1215
|
+
`Plugin protocol tape references missing frame '${frameId}'.`
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
currentFrameId = frame.id;
|
|
1219
|
+
record({
|
|
1220
|
+
kind: "frame-sent",
|
|
1221
|
+
frameId: frame.id,
|
|
1222
|
+
projectionDigest: frame.projectionDigest
|
|
1223
|
+
});
|
|
1224
|
+
sendHostPayload({
|
|
1225
|
+
type: "gameplay.frame",
|
|
1226
|
+
frame: frame.frame
|
|
1227
|
+
});
|
|
1228
|
+
};
|
|
1229
|
+
const drainHostFrames = () => {
|
|
1230
|
+
while (options.tape.steps[stepCursor]?.kind === "host.frame") {
|
|
1231
|
+
const step = options.tape.steps[stepCursor];
|
|
1232
|
+
stepCursor += 1;
|
|
1233
|
+
publishFrame(step.frameId);
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
const drainOneHostFrame = () => {
|
|
1237
|
+
if (options.tape.steps[stepCursor]?.kind !== "host.frame") {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const step = options.tape.steps[stepCursor];
|
|
1241
|
+
stepCursor += 1;
|
|
1242
|
+
publishFrame(step.frameId);
|
|
1243
|
+
};
|
|
1244
|
+
const waitForConfiguredLatency = async () => {
|
|
1245
|
+
if (latencyMs <= 0) {
|
|
1246
|
+
await Promise.resolve();
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
await new Promise((resolve) => setTimeout(resolve, latencyMs));
|
|
1250
|
+
};
|
|
1251
|
+
const consumeExpectedStep = (kind, requestDigest) => {
|
|
1252
|
+
const step = options.tape.steps[stepCursor];
|
|
1253
|
+
if (!step) {
|
|
1254
|
+
throw createFixtureError(
|
|
1255
|
+
"fixture-protocol-exhausted",
|
|
1256
|
+
`Plugin protocol tape has no remaining step for ${kind}.`
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
if (step.kind !== kind) {
|
|
1260
|
+
throw createFixtureError(
|
|
1261
|
+
"fixture-protocol-step-mismatch",
|
|
1262
|
+
`Expected protocol step '${step.kind}' but received '${kind}'.`
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
if (step.fromFrameId !== currentFrameId) {
|
|
1266
|
+
throw createFixtureError(
|
|
1267
|
+
"fixture-protocol-frame-mismatch",
|
|
1268
|
+
`Expected protocol from frame '${step.fromFrameId}' but current frame is '${currentFrameId ?? "<none>"}'.`
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
if (step.requestDigest !== requestDigest) {
|
|
1272
|
+
const mode = strict ? "strict" : "compatible";
|
|
1273
|
+
throw createFixtureError(
|
|
1274
|
+
"fixture-protocol-digest-mismatch",
|
|
1275
|
+
`${mode} fixture protocol digest mismatch for ${kind}: expected ${step.requestDigest}, received ${requestDigest}.`
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
stepCursor += 1;
|
|
1279
|
+
return step;
|
|
1280
|
+
};
|
|
1281
|
+
const handleValidate = async (command) => {
|
|
1282
|
+
const requestDigest = digestUIFixtureTransportRequest({
|
|
1283
|
+
operation: "validate",
|
|
1284
|
+
basis: command.basis,
|
|
1285
|
+
interactionId: command.interactionId,
|
|
1286
|
+
payload: command.params
|
|
1287
|
+
});
|
|
1288
|
+
const step = consumeExpectedStep("client.validate", requestDigest);
|
|
1289
|
+
record({
|
|
1290
|
+
kind: "validate-received",
|
|
1291
|
+
requestDigest,
|
|
1292
|
+
result: step.response.valid ? "accepted" : "rejected"
|
|
1293
|
+
});
|
|
1294
|
+
await waitForConfiguredLatency();
|
|
1295
|
+
sendHostPayload({
|
|
1296
|
+
type: "interaction.validation-result",
|
|
1297
|
+
requestId: command.requestId,
|
|
1298
|
+
result: step.response
|
|
1299
|
+
});
|
|
1300
|
+
};
|
|
1301
|
+
const handleSubmit = async (command) => {
|
|
1302
|
+
const requestDigest = digestUIFixtureTransportRequest({
|
|
1303
|
+
operation: "submit",
|
|
1304
|
+
basis: command.basis,
|
|
1305
|
+
interactionId: command.interactionId,
|
|
1306
|
+
payload: command.params
|
|
1307
|
+
});
|
|
1308
|
+
const step = consumeExpectedStep("client.submit", requestDigest);
|
|
1309
|
+
record({
|
|
1310
|
+
kind: "submit-received",
|
|
1311
|
+
requestDigest,
|
|
1312
|
+
result: step.response.accepted ? "accepted" : "rejected"
|
|
1313
|
+
});
|
|
1314
|
+
await waitForConfiguredLatency();
|
|
1315
|
+
sendHostPayload({
|
|
1316
|
+
type: "interaction.submit-result",
|
|
1317
|
+
requestId: command.requestId,
|
|
1318
|
+
result: step.response
|
|
1319
|
+
});
|
|
1320
|
+
if (step.response.accepted) {
|
|
1321
|
+
await waitForConfiguredLatency();
|
|
1322
|
+
drainHostFrames();
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
const handleClientPayload = (payload) => {
|
|
1326
|
+
const trackHandler = (promise) => {
|
|
1327
|
+
pendingHandlers.add(promise);
|
|
1328
|
+
promise.catch((error) => {
|
|
1329
|
+
rememberFixtureError(error);
|
|
1330
|
+
}).finally(() => {
|
|
1331
|
+
pendingHandlers.delete(promise);
|
|
1332
|
+
});
|
|
1333
|
+
};
|
|
1334
|
+
switch (payload.type) {
|
|
1335
|
+
case "runtime.ready":
|
|
1336
|
+
record({ kind: "ready-received" });
|
|
1337
|
+
break;
|
|
1338
|
+
case "runtime.ack":
|
|
1339
|
+
record({ kind: "ack-received" });
|
|
1340
|
+
break;
|
|
1341
|
+
case "interaction.validate":
|
|
1342
|
+
trackHandler(handleValidate(payload));
|
|
1343
|
+
break;
|
|
1344
|
+
case "interaction.submit":
|
|
1345
|
+
trackHandler(handleSubmit(payload));
|
|
1346
|
+
break;
|
|
1347
|
+
case "runtime.error":
|
|
1348
|
+
record({ kind: "diagnostic" });
|
|
1349
|
+
break;
|
|
1350
|
+
default: {
|
|
1351
|
+
const _exhaustive = payload;
|
|
1352
|
+
return _exhaustive;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
const transport = {
|
|
1357
|
+
start(nextOnMessage) {
|
|
1358
|
+
onMessage = nextOnMessage;
|
|
1359
|
+
queueMicrotask(() => {
|
|
1360
|
+
sendInit();
|
|
1361
|
+
drainOneHostFrame();
|
|
1362
|
+
});
|
|
1363
|
+
return () => {
|
|
1364
|
+
onMessage = null;
|
|
1365
|
+
};
|
|
1366
|
+
},
|
|
1367
|
+
send(message) {
|
|
1368
|
+
handleClientPayload(message);
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
return {
|
|
1372
|
+
transport,
|
|
1373
|
+
tape: options.tape,
|
|
1374
|
+
reset() {
|
|
1375
|
+
eventSequence = 0;
|
|
1376
|
+
envelopeSequence = 0;
|
|
1377
|
+
stepCursor = 0;
|
|
1378
|
+
currentFrameId = null;
|
|
1379
|
+
firstFixtureError = null;
|
|
1380
|
+
pendingHandlers.clear();
|
|
1381
|
+
events.length = 0;
|
|
1382
|
+
if (onMessage) {
|
|
1383
|
+
sendInit();
|
|
1384
|
+
drainOneHostFrame();
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
async flush() {
|
|
1388
|
+
await waitForConfiguredLatency();
|
|
1389
|
+
await Promise.resolve();
|
|
1390
|
+
while (pendingHandlers.size > 0) {
|
|
1391
|
+
await Promise.allSettled([...pendingHandlers]);
|
|
1392
|
+
}
|
|
1393
|
+
throwFixtureError();
|
|
1394
|
+
},
|
|
1395
|
+
async advanceHost() {
|
|
1396
|
+
await waitForConfiguredLatency();
|
|
1397
|
+
drainHostFrames();
|
|
1398
|
+
throwFixtureError();
|
|
1399
|
+
},
|
|
1400
|
+
getCurrentFrameId() {
|
|
1401
|
+
if (!currentFrameId) {
|
|
1402
|
+
throw createFixtureError(
|
|
1403
|
+
"fixture-frame-not-started",
|
|
1404
|
+
"Fixture host has not emitted a gameplay frame."
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
return currentFrameId;
|
|
665
1408
|
},
|
|
666
|
-
|
|
1409
|
+
getEvents() {
|
|
1410
|
+
return events.map((event) => ({ ...event }));
|
|
1411
|
+
},
|
|
1412
|
+
assertConsumed() {
|
|
1413
|
+
throwFixtureError();
|
|
1414
|
+
if (stepCursor !== options.tape.steps.length) {
|
|
1415
|
+
throw createFixtureError(
|
|
1416
|
+
"fixture-unconsumed-protocol-steps",
|
|
1417
|
+
`Plugin protocol tape consumed ${stepCursor} of ${options.tape.steps.length} protocol step(s).`
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// src/testing/ui-fixture/FixturePluginRuntime.tsx
|
|
1425
|
+
import { useMemo } from "react";
|
|
1426
|
+
import { jsx } from "react/jsx-runtime";
|
|
1427
|
+
function FixturePluginRuntime({
|
|
1428
|
+
harness,
|
|
1429
|
+
runtime,
|
|
1430
|
+
children
|
|
1431
|
+
}) {
|
|
1432
|
+
const fixtureRuntime = useMemo(
|
|
1433
|
+
() => runtime ?? createPluginRuntimeClient({ transport: harness.transport }),
|
|
1434
|
+
[harness.transport, runtime]
|
|
1435
|
+
);
|
|
1436
|
+
return /* @__PURE__ */ jsx(PluginRuntimeBoundary, { runtime: fixtureRuntime, children });
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/testing/ui-fixture/compile-plugin-protocol-tape.ts
|
|
1440
|
+
function compilePluginProtocolTape(options) {
|
|
1441
|
+
const frames = options.trace.frames.map(
|
|
1442
|
+
(frame) => materializeTraceFrame(frame, options.trace.viewer.playerId)
|
|
1443
|
+
);
|
|
1444
|
+
const frameById = new Map(frames.map((frame) => [frame.id, frame]));
|
|
1445
|
+
const steps = [];
|
|
1446
|
+
const firstFrame = frames[0];
|
|
1447
|
+
if (!firstFrame) {
|
|
1448
|
+
throw new Error(
|
|
1449
|
+
`Reducer scenario '${options.trace.scenarioId}' did not record frames.`
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
steps.push({
|
|
1453
|
+
id: `${firstFrame.id}.host-frame`,
|
|
1454
|
+
kind: "host.frame",
|
|
1455
|
+
frameId: firstFrame.id
|
|
1456
|
+
});
|
|
1457
|
+
for (const exchange of options.trace.exchanges) {
|
|
1458
|
+
const fromFrame = frameById.get(exchange.fromFrameId);
|
|
1459
|
+
if (!fromFrame) {
|
|
1460
|
+
throw new Error(
|
|
1461
|
+
`Reducer exchange '${exchange.id}' references missing frame '${exchange.fromFrameId}'.`
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
if (exchange.operation === "validate") {
|
|
1465
|
+
steps.push({
|
|
1466
|
+
id: exchange.id,
|
|
1467
|
+
kind: "client.validate",
|
|
1468
|
+
fromFrameId: exchange.fromFrameId,
|
|
1469
|
+
requestDigest: digestRuntimeCommand(exchange, fromFrame),
|
|
1470
|
+
response: {
|
|
1471
|
+
valid: exchange.result.valid,
|
|
1472
|
+
errorCode: exchange.result.errorCode,
|
|
1473
|
+
message: exchange.result.message
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
steps.push({
|
|
1479
|
+
id: exchange.id,
|
|
1480
|
+
kind: "client.submit",
|
|
1481
|
+
fromFrameId: exchange.fromFrameId,
|
|
1482
|
+
requestDigest: digestRuntimeCommand(exchange, fromFrame),
|
|
1483
|
+
response: exchange.result.kind === "accepted" ? { accepted: true } : {
|
|
1484
|
+
accepted: false,
|
|
1485
|
+
errorCode: exchange.result.diagnostics[0]?.code ?? "fixture-rejected",
|
|
1486
|
+
message: exchange.result.diagnostics[0]?.message
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
if (exchange.result.kind === "accepted") {
|
|
1490
|
+
if (!frameById.has(exchange.result.toFrameId)) {
|
|
1491
|
+
throw new Error(
|
|
1492
|
+
`Reducer exchange '${exchange.id}' references missing toFrameId '${exchange.result.toFrameId}'.`
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
steps.push({
|
|
1496
|
+
id: `${exchange.id}.host-frame`,
|
|
1497
|
+
kind: "host.frame",
|
|
1498
|
+
frameId: exchange.result.toFrameId
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return {
|
|
1503
|
+
session: options.session,
|
|
1504
|
+
frames,
|
|
1505
|
+
steps
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
function materializeTraceFrame(frame, perspectivePlayerId) {
|
|
1509
|
+
const flow = readFlowState3(frame.reducerState);
|
|
1510
|
+
return {
|
|
1511
|
+
id: frame.id,
|
|
1512
|
+
frame: materializePluginGameplayFrame({
|
|
1513
|
+
currentPhase: flow.currentPhase,
|
|
1514
|
+
activePlayers: flow.activePlayers,
|
|
1515
|
+
dynamicProjection: frame.dynamicProjection,
|
|
1516
|
+
staticProjection: frame.staticProjection,
|
|
1517
|
+
perspectivePlayerId,
|
|
1518
|
+
gameVersion: frame.gameVersion,
|
|
1519
|
+
actionSetVersion: frame.actionSetVersion
|
|
1520
|
+
}),
|
|
1521
|
+
projectionDigest: frame.projectionDigest
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
function digestRuntimeCommand(exchange, frame) {
|
|
1525
|
+
return digestUIFixtureTransportRequest({
|
|
1526
|
+
operation: exchange.operation === "validate" ? "validate" : "submit",
|
|
1527
|
+
basis: {
|
|
1528
|
+
gameVersion: frame.frame.gameVersion,
|
|
1529
|
+
actionSetVersion: frame.frame.actionSetVersion,
|
|
1530
|
+
perspectivePlayerId: frame.frame.perspectivePlayerId
|
|
1531
|
+
},
|
|
1532
|
+
interactionId: exchange.input.interactionId,
|
|
1533
|
+
payload: exchange.input.params
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
function readFlowState3(state) {
|
|
1537
|
+
return {
|
|
1538
|
+
currentPhase: state.domain?.flow?.currentPhase ?? null,
|
|
1539
|
+
activePlayers: [...state.domain?.flow?.activePlayers ?? []]
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// src/testing/ui-fixture/deterministic-environment.ts
|
|
1544
|
+
var defaultFixtureEnvironmentInit = {
|
|
1545
|
+
clockIso: "2026-01-01T00:00:00.000Z",
|
|
1546
|
+
randomSeed: "dreamboard-ui-fixture",
|
|
1547
|
+
locale: "en-US",
|
|
1548
|
+
timezone: "UTC",
|
|
1549
|
+
reducedMotion: true,
|
|
1550
|
+
network: "blocked"
|
|
1551
|
+
};
|
|
1552
|
+
function createDeterministicIdFactory(seed) {
|
|
1553
|
+
let cursor = 0;
|
|
1554
|
+
return () => {
|
|
1555
|
+
cursor += 1;
|
|
1556
|
+
return `fixture-${slug(seed)}-${cursor.toString(36).padStart(4, "0")}`;
|
|
667
1557
|
};
|
|
668
1558
|
}
|
|
1559
|
+
function fixtureEnvironmentInitFor(options) {
|
|
1560
|
+
return {
|
|
1561
|
+
...defaultFixtureEnvironmentInit,
|
|
1562
|
+
clockIso: options.clockIso,
|
|
1563
|
+
randomSeed: options.randomSeed
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function slug(value) {
|
|
1567
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 48) || "seed";
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// src/testing/ui-fixture/schema.ts
|
|
1571
|
+
import { z } from "zod";
|
|
1572
|
+
var UI_SCENARIO_FIXTURE_SCHEMA_VERSION = 2;
|
|
1573
|
+
var UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION = 2;
|
|
1574
|
+
var UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR = 3;
|
|
1575
|
+
var UI_SCENARIO_FIXTURE_PLUGIN_RUNTIME_PROTOCOL = DREAMBOARD_PLUGIN_PROTOCOL_VERSION;
|
|
1576
|
+
var digestSchema = z.string().refine(isSha256Digest, {
|
|
1577
|
+
message: "Expected a sha256:<64 lowercase hex> digest."
|
|
1578
|
+
});
|
|
1579
|
+
var nonEmptyStringSchema = z.string().min(1);
|
|
1580
|
+
var jsonSchema = RuntimeJsonSchema;
|
|
1581
|
+
var environmentSchema = z.object({
|
|
1582
|
+
clockIso: nonEmptyStringSchema,
|
|
1583
|
+
randomSeed: nonEmptyStringSchema,
|
|
1584
|
+
locale: z.literal("en-US"),
|
|
1585
|
+
timezone: z.literal("UTC"),
|
|
1586
|
+
viewportTags: z.array(z.enum(["desktop", "tablet", "phone", "touch"]))
|
|
1587
|
+
}).strict();
|
|
1588
|
+
var uiFixtureFrameSchema = PluginProtocolFrameSchema;
|
|
1589
|
+
var uiFixtureProtocolStepSchema = PluginProtocolStepSchema;
|
|
1590
|
+
var pluginProtocolTapeSchema = PluginProtocolTapeSchema.refine(
|
|
1591
|
+
(tape) => tape.frames.length > 0,
|
|
1592
|
+
"UI fixture protocol tapes require at least one gameplay frame."
|
|
1593
|
+
);
|
|
1594
|
+
var browserInteractionIntentRequestSchema = z.object({
|
|
1595
|
+
surface: nonEmptyStringSchema,
|
|
1596
|
+
scopeId: nonEmptyStringSchema.optional(),
|
|
1597
|
+
interactionKey: nonEmptyStringSchema.optional(),
|
|
1598
|
+
interactionId: nonEmptyStringSchema.optional(),
|
|
1599
|
+
intent: nonEmptyStringSchema,
|
|
1600
|
+
inputKey: nonEmptyStringSchema.optional(),
|
|
1601
|
+
candidateValue: jsonSchema.optional(),
|
|
1602
|
+
candidateValueKey: nonEmptyStringSchema.optional(),
|
|
1603
|
+
actuatorKind: z.enum(BROWSER_INTERACTION_ACTUATOR_KINDS).optional(),
|
|
1604
|
+
allowDisabled: z.boolean().optional()
|
|
1605
|
+
}).strict();
|
|
1606
|
+
var browserInteractionEffectRequestSchema = z.object({
|
|
1607
|
+
surface: nonEmptyStringSchema,
|
|
1608
|
+
scopeId: nonEmptyStringSchema.optional(),
|
|
1609
|
+
interactionKey: nonEmptyStringSchema.optional(),
|
|
1610
|
+
interactionId: nonEmptyStringSchema.optional(),
|
|
1611
|
+
effect: browserInteractionEffectSchema,
|
|
1612
|
+
allowDisabled: z.boolean().optional()
|
|
1613
|
+
}).strict();
|
|
1614
|
+
var uiReplayRequestSchema = z.union([
|
|
1615
|
+
browserInteractionIntentRequestSchema,
|
|
1616
|
+
browserInteractionEffectRequestSchema
|
|
1617
|
+
]);
|
|
1618
|
+
var uiReplayExecutionSchema = z.union([
|
|
1619
|
+
z.object({
|
|
1620
|
+
kind: z.literal("activate")
|
|
1621
|
+
}).strict(),
|
|
1622
|
+
z.object({
|
|
1623
|
+
kind: z.literal("fill"),
|
|
1624
|
+
value: z.string()
|
|
1625
|
+
}).strict(),
|
|
1626
|
+
z.object({
|
|
1627
|
+
kind: z.literal("drag"),
|
|
1628
|
+
target: browserInteractionEffectRequestSchema
|
|
1629
|
+
}).strict()
|
|
1630
|
+
]);
|
|
1631
|
+
var uiStepExpectationSchema = z.object({
|
|
1632
|
+
frameId: nonEmptyStringSchema.optional(),
|
|
1633
|
+
projectionDigest: digestSchema.optional(),
|
|
1634
|
+
semanticDigest: digestSchema.optional(),
|
|
1635
|
+
draftDigest: digestSchema.optional(),
|
|
1636
|
+
submissionDigest: digestSchema.optional(),
|
|
1637
|
+
focusedInteractionKey: nonEmptyStringSchema.optional(),
|
|
1638
|
+
visibleInteractionKeys: z.array(nonEmptyStringSchema).optional()
|
|
1639
|
+
}).strict();
|
|
1640
|
+
var uiResolvedReplayIdentitySchema = z.object({
|
|
1641
|
+
stepId: nonEmptyStringSchema,
|
|
1642
|
+
surface: nonEmptyStringSchema,
|
|
1643
|
+
scopeId: nonEmptyStringSchema,
|
|
1644
|
+
interactionKey: nonEmptyStringSchema,
|
|
1645
|
+
interactionId: nonEmptyStringSchema,
|
|
1646
|
+
actuatorId: nonEmptyStringSchema,
|
|
1647
|
+
descriptorDigest: digestSchema.optional(),
|
|
1648
|
+
draftDigest: digestSchema.optional()
|
|
1649
|
+
}).strict();
|
|
1650
|
+
var portableSemanticReplayStepSchema = z.object({
|
|
1651
|
+
stepId: nonEmptyStringSchema,
|
|
1652
|
+
requestDigest: digestSchema,
|
|
1653
|
+
resolve: uiReplayRequestSchema,
|
|
1654
|
+
execute: uiReplayExecutionSchema,
|
|
1655
|
+
expectedIdentity: uiResolvedReplayIdentitySchema.optional(),
|
|
1656
|
+
expect: uiStepExpectationSchema
|
|
1657
|
+
}).strict();
|
|
1658
|
+
var uiScenarioReplayStepSchema = z.union([
|
|
1659
|
+
portableSemanticReplayStepSchema,
|
|
1660
|
+
z.object({
|
|
1661
|
+
stepId: nonEmptyStringSchema,
|
|
1662
|
+
kind: z.literal("assert"),
|
|
1663
|
+
expect: uiStepExpectationSchema
|
|
1664
|
+
}).strict()
|
|
1665
|
+
]);
|
|
1666
|
+
var uiScenarioFixtureSchema = z.object({
|
|
1667
|
+
schemaVersion: z.literal(UI_SCENARIO_FIXTURE_SCHEMA_VERSION),
|
|
1668
|
+
browserInteractionProtocol: z.literal(
|
|
1669
|
+
DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION
|
|
1670
|
+
),
|
|
1671
|
+
pluginRuntimeProtocol: z.literal(DREAMBOARD_PLUGIN_PROTOCOL_VERSION),
|
|
1672
|
+
id: nonEmptyStringSchema,
|
|
1673
|
+
title: nonEmptyStringSchema,
|
|
1674
|
+
gameId: nonEmptyStringSchema,
|
|
1675
|
+
tags: z.array(nonEmptyStringSchema),
|
|
1676
|
+
source: z.object({
|
|
1677
|
+
scenarioId: nonEmptyStringSchema,
|
|
1678
|
+
reducerFingerprint: nonEmptyStringSchema,
|
|
1679
|
+
uiContractFingerprint: nonEmptyStringSchema,
|
|
1680
|
+
renderModule: nonEmptyStringSchema,
|
|
1681
|
+
renderModuleDigest: digestSchema,
|
|
1682
|
+
sourceDigest: digestSchema
|
|
1683
|
+
}).strict(),
|
|
1684
|
+
viewer: z.object({
|
|
1685
|
+
seatId: nonEmptyStringSchema,
|
|
1686
|
+
playerId: nonEmptyStringSchema.optional()
|
|
1687
|
+
}).strict(),
|
|
1688
|
+
environment: environmentSchema,
|
|
1689
|
+
protocol: pluginProtocolTapeSchema,
|
|
1690
|
+
replay: z.array(uiScenarioReplayStepSchema),
|
|
1691
|
+
expected: z.object({
|
|
1692
|
+
initialProjectionDigest: digestSchema,
|
|
1693
|
+
finalProjectionDigest: digestSchema,
|
|
1694
|
+
finalSemanticDigest: digestSchema,
|
|
1695
|
+
submissionDigest: digestSchema
|
|
1696
|
+
}).strict()
|
|
1697
|
+
}).strict();
|
|
1698
|
+
var uiScenarioFixtureBundleIndexSchema = z.object({
|
|
1699
|
+
schemaVersion: z.literal(UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION),
|
|
1700
|
+
bundleId: nonEmptyStringSchema,
|
|
1701
|
+
sdkCommit: nonEmptyStringSchema,
|
|
1702
|
+
pluginRuntimeProtocol: z.literal(DREAMBOARD_PLUGIN_PROTOCOL_VERSION),
|
|
1703
|
+
browserInteractionProtocol: z.literal(
|
|
1704
|
+
DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION
|
|
1705
|
+
),
|
|
1706
|
+
fixtures: z.array(
|
|
1707
|
+
z.object({
|
|
1708
|
+
id: nonEmptyStringSchema,
|
|
1709
|
+
file: nonEmptyStringSchema,
|
|
1710
|
+
sha256: digestSchema,
|
|
1711
|
+
renderModule: nonEmptyStringSchema,
|
|
1712
|
+
renderModuleSha256: digestSchema,
|
|
1713
|
+
components: z.array(nonEmptyStringSchema),
|
|
1714
|
+
capabilities: z.array(nonEmptyStringSchema)
|
|
1715
|
+
}).strict()
|
|
1716
|
+
)
|
|
1717
|
+
}).strict();
|
|
1718
|
+
function parseUIScenarioFixture(value) {
|
|
1719
|
+
const fixture = uiScenarioFixtureSchema.parse(
|
|
1720
|
+
value
|
|
1721
|
+
);
|
|
1722
|
+
validateUIScenarioFixtureReferences(fixture);
|
|
1723
|
+
return fixture;
|
|
1724
|
+
}
|
|
1725
|
+
function parseUIScenarioFixtureBundleIndex(value) {
|
|
1726
|
+
const bundle = uiScenarioFixtureBundleIndexSchema.parse(value);
|
|
1727
|
+
assertSupportedBrowserInteractionProtocol(bundle.browserInteractionProtocol);
|
|
1728
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1729
|
+
for (const entry of bundle.fixtures) {
|
|
1730
|
+
if (ids.has(entry.id)) {
|
|
1731
|
+
throw new Error(`Duplicate fixture id '${entry.id}' in bundle index.`);
|
|
1732
|
+
}
|
|
1733
|
+
ids.add(entry.id);
|
|
1734
|
+
}
|
|
1735
|
+
return bundle;
|
|
1736
|
+
}
|
|
1737
|
+
function assertSupportedBrowserInteractionProtocol(version) {
|
|
1738
|
+
const major = Number(version.split(".")[0]);
|
|
1739
|
+
if (major !== UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR) {
|
|
1740
|
+
throw new Error(
|
|
1741
|
+
`Unsupported browser-interaction protocol '${version}'. Expected major ${UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR}.`
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
function validateUIScenarioFixtureReferences(fixture) {
|
|
1746
|
+
assertSupportedBrowserInteractionProtocol(fixture.browserInteractionProtocol);
|
|
1747
|
+
const frameIds = /* @__PURE__ */ new Set();
|
|
1748
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
1749
|
+
for (const frame of fixture.protocol.frames) {
|
|
1750
|
+
if (frameIds.has(frame.id)) {
|
|
1751
|
+
throw new Error(`Duplicate fixture frame id '${frame.id}'.`);
|
|
1752
|
+
}
|
|
1753
|
+
frameIds.add(frame.id);
|
|
1754
|
+
}
|
|
1755
|
+
for (const step of fixture.protocol.steps) {
|
|
1756
|
+
if (stepIds.has(step.id)) {
|
|
1757
|
+
throw new Error(`Duplicate protocol step id '${step.id}'.`);
|
|
1758
|
+
}
|
|
1759
|
+
stepIds.add(step.id);
|
|
1760
|
+
if (step.kind === "host.frame") {
|
|
1761
|
+
if (!frameIds.has(step.frameId)) {
|
|
1762
|
+
throw new Error(
|
|
1763
|
+
`Protocol step '${step.id}' references missing frameId '${step.frameId}'.`
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
if (!frameIds.has(step.fromFrameId)) {
|
|
1769
|
+
throw new Error(
|
|
1770
|
+
`Protocol step '${step.id}' references missing fromFrameId '${step.fromFrameId}'.`
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
for (const step of fixture.replay) {
|
|
1775
|
+
if ("resolve" in step) {
|
|
1776
|
+
const actualDigest = digestUIFixtureRequest(
|
|
1777
|
+
step.resolve
|
|
1778
|
+
);
|
|
1779
|
+
if (step.requestDigest !== actualDigest) {
|
|
1780
|
+
throw new Error(
|
|
1781
|
+
`Replay step '${step.stepId}' requestDigest ${step.requestDigest} does not match ${actualDigest}.`
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
const expectedFrameId = step.expect.frameId;
|
|
1786
|
+
if (expectedFrameId && !frameIds.has(expectedFrameId)) {
|
|
1787
|
+
throw new Error(
|
|
1788
|
+
`Replay step '${step.stepId}' expects missing frameId '${expectedFrameId}'.`
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// src/testing/ui-fixture/compiler.ts
|
|
1795
|
+
function compileUIScenarioFixture(options) {
|
|
1796
|
+
const firstFrame = options.protocol.frames[0];
|
|
1797
|
+
const finalFrame = lastPublishedFrame(options.protocol);
|
|
1798
|
+
if (!firstFrame || !finalFrame) {
|
|
1799
|
+
throw new Error(
|
|
1800
|
+
"compileUIScenarioFixture requires at least one protocol frame."
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
const replay = options.replay ?? [];
|
|
1804
|
+
const submissionDigest = options.expected?.submissionDigest ?? digestUIFixtureJson({
|
|
1805
|
+
digestVersion: "ui-submission@1",
|
|
1806
|
+
fixtureId: options.id,
|
|
1807
|
+
replay: replay.map(
|
|
1808
|
+
(step) => "requestDigest" in step ? step.requestDigest : step.stepId
|
|
1809
|
+
)
|
|
1810
|
+
});
|
|
1811
|
+
const finalSemanticDigest = options.expected?.finalSemanticDigest ?? digestUIFixtureJson({
|
|
1812
|
+
digestVersion: "ui-semantic@1",
|
|
1813
|
+
fixtureId: options.id,
|
|
1814
|
+
frameId: finalFrame.id,
|
|
1815
|
+
projectionDigest: finalFrame.projectionDigest
|
|
1816
|
+
});
|
|
1817
|
+
return parseUIScenarioFixture(
|
|
1818
|
+
canonicalizeUIScenarioFixture({
|
|
1819
|
+
schemaVersion: 2,
|
|
1820
|
+
browserInteractionProtocol: DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION,
|
|
1821
|
+
pluginRuntimeProtocol: DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
|
|
1822
|
+
id: options.id,
|
|
1823
|
+
title: options.title,
|
|
1824
|
+
gameId: options.gameId,
|
|
1825
|
+
tags: [...options.tags ?? []],
|
|
1826
|
+
source: options.source,
|
|
1827
|
+
viewer: options.viewer,
|
|
1828
|
+
environment: options.environment,
|
|
1829
|
+
protocol: options.protocol,
|
|
1830
|
+
replay,
|
|
1831
|
+
expected: {
|
|
1832
|
+
initialProjectionDigest: options.expected?.initialProjectionDigest ?? firstFrame.projectionDigest,
|
|
1833
|
+
finalProjectionDigest: options.expected?.finalProjectionDigest ?? finalFrame.projectionDigest,
|
|
1834
|
+
finalSemanticDigest,
|
|
1835
|
+
submissionDigest
|
|
1836
|
+
}
|
|
1837
|
+
})
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
function lastPublishedFrame(protocol) {
|
|
1841
|
+
const frameById = new Map(protocol.frames.map((frame) => [frame.id, frame]));
|
|
1842
|
+
for (let index = protocol.steps.length - 1; index >= 0; index -= 1) {
|
|
1843
|
+
const step = protocol.steps[index];
|
|
1844
|
+
if (step?.kind === "host.frame") {
|
|
1845
|
+
return frameById.get(step.frameId);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
return protocol.frames[protocol.frames.length - 1];
|
|
1849
|
+
}
|
|
1850
|
+
function assertDeterministicUIScenarioFixture(first, second) {
|
|
1851
|
+
const firstBytes = serializeUIScenarioFixture(first);
|
|
1852
|
+
const secondBytes = serializeUIScenarioFixture(second);
|
|
1853
|
+
if (firstBytes !== secondBytes) {
|
|
1854
|
+
throw new Error(
|
|
1855
|
+
`UI scenario fixture '${first.id}' is non-deterministic across compilation runs.`
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
function assertUniqueReplayIdentity(step, candidates) {
|
|
1860
|
+
if (candidates.length !== 1) {
|
|
1861
|
+
throw new Error(
|
|
1862
|
+
`Replay step '${step.stepId}' resolved ${candidates.length} identities; expected exactly one.`
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
const candidate = candidates[0];
|
|
1866
|
+
if (!candidate) {
|
|
1867
|
+
throw new Error(`Replay step '${step.stepId}' did not resolve.`);
|
|
1868
|
+
}
|
|
1869
|
+
const expected = step.expectedIdentity;
|
|
1870
|
+
if (!expected) {
|
|
1871
|
+
return candidate;
|
|
1872
|
+
}
|
|
1873
|
+
for (const key of [
|
|
1874
|
+
"surface",
|
|
1875
|
+
"scopeId",
|
|
1876
|
+
"interactionKey",
|
|
1877
|
+
"interactionId",
|
|
1878
|
+
"actuatorId",
|
|
1879
|
+
"descriptorDigest",
|
|
1880
|
+
"draftDigest"
|
|
1881
|
+
]) {
|
|
1882
|
+
const expectedValue = expected[key];
|
|
1883
|
+
if (expectedValue !== void 0 && candidate[key] !== expectedValue) {
|
|
1884
|
+
throw new Error(
|
|
1885
|
+
`Replay step '${step.stepId}' resolved ${key} '${String(
|
|
1886
|
+
candidate[key]
|
|
1887
|
+
)}', expected '${expectedValue}'.`
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
return candidate;
|
|
1892
|
+
}
|
|
1893
|
+
function assertUIStepExpectationSatisfied(stepId, expected, actual) {
|
|
1894
|
+
for (const key of [
|
|
1895
|
+
"frameId",
|
|
1896
|
+
"projectionDigest",
|
|
1897
|
+
"semanticDigest",
|
|
1898
|
+
"draftDigest",
|
|
1899
|
+
"submissionDigest",
|
|
1900
|
+
"focusedInteractionKey"
|
|
1901
|
+
]) {
|
|
1902
|
+
if (expected[key] !== void 0 && expected[key] !== actual[key]) {
|
|
1903
|
+
throw new Error(
|
|
1904
|
+
`Replay step '${stepId}' expected ${key} '${expected[key]}', received '${actual[key]}'.`
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
if (expected.visibleInteractionKeys) {
|
|
1909
|
+
const actualKeys = new Set(actual.visibleInteractionKeys ?? []);
|
|
1910
|
+
const missing = expected.visibleInteractionKeys.filter(
|
|
1911
|
+
(key) => !actualKeys.has(key)
|
|
1912
|
+
);
|
|
1913
|
+
if (missing.length > 0) {
|
|
1914
|
+
throw new Error(
|
|
1915
|
+
`Replay step '${stepId}' missing visible interactions: ${missing.join(
|
|
1916
|
+
", "
|
|
1917
|
+
)}.`
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
669
1922
|
export {
|
|
1923
|
+
FixturePluginRuntime,
|
|
670
1924
|
StaleContractArtifactError,
|
|
1925
|
+
UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION,
|
|
1926
|
+
UI_SCENARIO_FIXTURE_PLUGIN_RUNTIME_PROTOCOL,
|
|
1927
|
+
UI_SCENARIO_FIXTURE_SCHEMA_VERSION,
|
|
1928
|
+
UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR,
|
|
1929
|
+
assertDeterministicUIScenarioFixture,
|
|
1930
|
+
assertSupportedBrowserInteractionProtocol,
|
|
1931
|
+
assertUIStepExpectationSatisfied,
|
|
1932
|
+
assertUniqueReplayIdentity,
|
|
1933
|
+
canonicalUIFixtureJson,
|
|
1934
|
+
canonicalizeUIFixtureJson,
|
|
1935
|
+
canonicalizeUIScenarioFixture,
|
|
1936
|
+
compareUIParityObservations,
|
|
1937
|
+
compilePluginProtocolTape,
|
|
1938
|
+
compileUIScenarioFixture,
|
|
1939
|
+
createDeterministicIdFactory,
|
|
671
1940
|
createExpectApi,
|
|
1941
|
+
createFixtureHostHarness,
|
|
1942
|
+
createReducerScenarioRunner,
|
|
672
1943
|
createTestRuntime,
|
|
1944
|
+
createUIParityObservationFromFixture,
|
|
1945
|
+
defaultFixtureEnvironmentInit,
|
|
673
1946
|
defineBase,
|
|
674
1947
|
defineScenario,
|
|
675
|
-
|
|
1948
|
+
digestPluginProtocolTape,
|
|
1949
|
+
digestUIFixtureJson,
|
|
1950
|
+
digestUIFixtureRequest,
|
|
1951
|
+
digestUIFixtureTransportRequest,
|
|
1952
|
+
digestUIScenarioFixture,
|
|
1953
|
+
fixtureEnvironmentInitFor,
|
|
1954
|
+
isSha256Digest,
|
|
1955
|
+
isStaleContractArtifactError,
|
|
1956
|
+
parseUIParityObservationV1,
|
|
1957
|
+
parseUIScenarioFixture,
|
|
1958
|
+
parseUIScenarioFixtureBundleIndex,
|
|
1959
|
+
pluginProtocolTapeSchema,
|
|
1960
|
+
portableSemanticReplayStepSchema,
|
|
1961
|
+
serializeUIScenarioFixture,
|
|
1962
|
+
uiFixtureFrameSchema,
|
|
1963
|
+
uiFixtureProtocolStepSchema,
|
|
1964
|
+
uiReplayExecutionSchema,
|
|
1965
|
+
uiReplayRequestSchema,
|
|
1966
|
+
uiResolvedReplayIdentitySchema,
|
|
1967
|
+
uiScenarioFixtureBundleIndexSchema,
|
|
1968
|
+
uiScenarioFixtureSchema,
|
|
1969
|
+
uiScenarioReplayStepSchema,
|
|
1970
|
+
uiStepExpectationSchema
|
|
676
1971
|
};
|
|
677
1972
|
//# sourceMappingURL=testing.js.map
|