@dreamboard-games/sdk 0.4.0-alpha.1 → 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/REFERENCE.md +1215 -125
- 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-BrZtP9az.d.ts → attributes-DbvyMbXw.d.ts} +17 -2
- 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 +205 -5
- package/dist/browser-interaction.js +14 -2
- package/dist/{chunk-7W6TQO3X.js → chunk-45FI6XII.js} +613 -366
- package/dist/chunk-45FI6XII.js.map +1 -0
- package/dist/{chunk-3K362ZKR.js → chunk-6JQFGSLD.js} +202 -38
- package/dist/chunk-6JQFGSLD.js.map +1 -0
- package/dist/{chunk-XXBSIPJX.js → chunk-CP35NPPY.js} +131 -59
- package/dist/chunk-CP35NPPY.js.map +1 -0
- package/dist/{chunk-XJYUSD5F.js → chunk-FWVV2X74.js} +2 -2
- package/dist/{chunk-IUGKW2GS.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-7LZZEI25.js → chunk-N6ZDIMRB.js} +1 -1
- 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-AEUXCFGM.js → chunk-PF7L4BMG.js} +1 -8
- package/dist/{chunk-AEUXCFGM.js.map → chunk-PF7L4BMG.js.map} +1 -1
- package/dist/{chunk-CZMFAS3E.js → chunk-WOWFONLZ.js} +517 -1274
- package/dist/chunk-WOWFONLZ.js.map +1 -0
- package/dist/{chunk-MALRKLRR.js → chunk-WUIVY2KE.js} +301 -12
- 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 -3
- package/dist/codegen.js +1 -1
- package/dist/{components-Dqr_LuZJ.d.ts → components-BbWPnZCF.d.ts} +37 -6
- 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.js +1 -1
- package/dist/reducer-contract.js +1 -1
- package/dist/reducer.js +2 -2
- package/dist/reducer.js.map +1 -1
- package/dist/runtime/primitives.d.ts +10 -10
- package/dist/runtime/primitives.js +9 -7
- package/dist/runtime/runtime-api.d.ts +2 -2
- package/dist/runtime/workspace-contract.d.ts +23 -12
- package/dist/runtime/workspace-contract.js +7 -6
- package/dist/runtime-api-BJharVaj.d.ts +198 -0
- package/dist/runtime-api-C6aWo9c0.d.ts +95 -0
- package/dist/runtime.d.ts +35 -31
- package/dist/runtime.js +87 -797
- package/dist/runtime.js.map +1 -1
- package/dist/testing.d.ts +254 -140
- package/dist/testing.js +964 -149
- package/dist/testing.js.map +1 -1
- package/dist/types-CYeN0wLK.d.ts +29 -0
- package/dist/{types-BqzHkNMA.d.ts → types-DJj5MJkl.d.ts} +32 -5
- package/dist/types.d.ts +13 -8
- 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-contract-W2-kxObe.d.ts → ui-contract-CY7cxsCC.d.ts} +16 -26
- package/dist/ui.d.ts +7 -8
- package/dist/ui.js +20 -2
- package/package.json +25 -19
- package/dist/chunk-3K362ZKR.js.map +0 -1
- package/dist/chunk-7LZZEI25.js.map +0 -1
- package/dist/chunk-7W6TQO3X.js.map +0 -1
- package/dist/chunk-CZMFAS3E.js.map +0 -1
- package/dist/chunk-IUGKW2GS.js.map +0 -1
- package/dist/chunk-MALRKLRR.js.map +0 -1
- package/dist/chunk-XXBSIPJX.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-XJYUSD5F.js.map → chunk-FWVV2X74.js.map} +0 -0
package/dist/testing.js
CHANGED
|
@@ -2,14 +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";
|
|
5
18
|
import {
|
|
6
19
|
BROWSER_INTERACTION_ACTUATOR_KINDS,
|
|
7
20
|
DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION,
|
|
8
21
|
browserInteractionEffectSchema
|
|
9
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-WUIVY2KE.js";
|
|
10
23
|
import {
|
|
11
24
|
RuntimeJsonSchema
|
|
12
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-PF7L4BMG.js";
|
|
13
26
|
import "./chunk-PZ5AY32C.js";
|
|
14
27
|
|
|
15
28
|
// src/testing/definitions.ts
|
|
@@ -37,9 +50,9 @@ function matchPartial(actual, expected, path = "value") {
|
|
|
37
50
|
if (!(key in actual)) {
|
|
38
51
|
return `${path}.${key} is missing`;
|
|
39
52
|
}
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
return
|
|
53
|
+
const mismatch2 = matchPartial(actualValue, expectedValue, `${path}.${key}`);
|
|
54
|
+
if (mismatch2) {
|
|
55
|
+
return mismatch2;
|
|
43
56
|
}
|
|
44
57
|
}
|
|
45
58
|
return null;
|
|
@@ -59,9 +72,9 @@ function assertDescriptorMatches(descriptor, opts) {
|
|
|
59
72
|
if (!opts) {
|
|
60
73
|
return;
|
|
61
74
|
}
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
throw new Error(
|
|
75
|
+
const mismatch2 = matchPartial(descriptor, opts, "interaction");
|
|
76
|
+
if (mismatch2) {
|
|
77
|
+
throw new Error(mismatch2);
|
|
65
78
|
}
|
|
66
79
|
}
|
|
67
80
|
function createSubmissionErrorMatcher(actual, expected, options) {
|
|
@@ -178,9 +191,9 @@ function createExpectApi(options = {}) {
|
|
|
178
191
|
}
|
|
179
192
|
},
|
|
180
193
|
toMatchObject: (expected) => {
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
throw new Error(
|
|
194
|
+
const mismatch2 = matchPartial(actual, expected);
|
|
195
|
+
if (mismatch2) {
|
|
196
|
+
throw new Error(mismatch2);
|
|
184
197
|
}
|
|
185
198
|
},
|
|
186
199
|
toBeDefined: () => {
|
|
@@ -422,7 +435,7 @@ function resolvePlayerIds(options) {
|
|
|
422
435
|
function isWireZoneHandles(value) {
|
|
423
436
|
return typeof value === "object" && value !== null;
|
|
424
437
|
}
|
|
425
|
-
function
|
|
438
|
+
function buildPluginFrame(options) {
|
|
426
439
|
const projection = options.bundle.projectSeatsDynamic({
|
|
427
440
|
state: options.state,
|
|
428
441
|
playerIds: [options.playerId]
|
|
@@ -457,28 +470,25 @@ function buildPluginSnapshot(options) {
|
|
|
457
470
|
];
|
|
458
471
|
})
|
|
459
472
|
);
|
|
473
|
+
const availableInteractions = hydrateRefs(
|
|
474
|
+
seat?.availableInteractionRefs
|
|
475
|
+
);
|
|
460
476
|
return {
|
|
477
|
+
gameVersion: options.version,
|
|
478
|
+
actionSetVersion: computePluginActionSetVersion({
|
|
479
|
+
gameVersion: options.version,
|
|
480
|
+
availableInteractions
|
|
481
|
+
}),
|
|
482
|
+
perspectivePlayerId: options.playerId || null,
|
|
461
483
|
view: seat?.view ?? null,
|
|
462
|
-
|
|
484
|
+
flow: {
|
|
463
485
|
currentPhase: flow.currentPhase,
|
|
464
486
|
currentStage: projection.currentStage ?? null,
|
|
465
487
|
activePlayers: flow.activePlayers,
|
|
466
|
-
simultaneousPhase: projection.simultaneousPhase ?? null
|
|
467
|
-
availableInteractions: hydrateRefs(
|
|
468
|
-
seat?.availableInteractionRefs
|
|
469
|
-
),
|
|
470
|
-
zones
|
|
471
|
-
},
|
|
472
|
-
lobby: null,
|
|
473
|
-
notifications: [],
|
|
474
|
-
session: {
|
|
475
|
-
sessionId: options.sessionId,
|
|
476
|
-
controllablePlayerIds: [...options.playerIds],
|
|
477
|
-
controllingPlayerId: options.playerId || null,
|
|
478
|
-
userId: options.userId
|
|
488
|
+
simultaneousPhase: projection.simultaneousPhase ?? null
|
|
479
489
|
},
|
|
480
|
-
|
|
481
|
-
|
|
490
|
+
availableInteractions,
|
|
491
|
+
zones
|
|
482
492
|
};
|
|
483
493
|
}
|
|
484
494
|
function createTestRuntime(options) {
|
|
@@ -499,44 +509,37 @@ function createTestRuntime(options) {
|
|
|
499
509
|
baseState,
|
|
500
510
|
explicitPlayerIds: options.playerIds
|
|
501
511
|
});
|
|
502
|
-
const userId = options.userId ?? "test-user";
|
|
503
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
|
+
};
|
|
504
520
|
let version = 0;
|
|
505
521
|
let submissionCounter = 0;
|
|
506
522
|
let currentPlayerId = playerIds[0] ?? "";
|
|
507
523
|
const diagnosticEvents = [];
|
|
508
524
|
let lastDispatch = null;
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
status: "ready"
|
|
525
|
+
const toReadySessionState = (frame) => ({
|
|
526
|
+
status: "ready",
|
|
527
|
+
sessionId,
|
|
528
|
+
controllingPlayerId: frame.perspectivePlayerId
|
|
514
529
|
});
|
|
515
|
-
let
|
|
530
|
+
let lastPluginFrame;
|
|
516
531
|
let lastSessionState;
|
|
517
532
|
const applyCurrentState = () => {
|
|
518
|
-
const previousSession = lastSessionState;
|
|
519
533
|
version += 1;
|
|
520
|
-
|
|
534
|
+
lastPluginFrame = buildPluginFrame({
|
|
521
535
|
state: currentState,
|
|
522
536
|
bundle: options.bundle,
|
|
523
537
|
playerId: currentPlayerId,
|
|
524
|
-
playerIds,
|
|
525
|
-
userId,
|
|
526
|
-
sessionId,
|
|
527
538
|
version,
|
|
528
539
|
expectedPhase: version === 1 ? options.phase : void 0,
|
|
529
540
|
baseId: options.baseId
|
|
530
541
|
});
|
|
531
|
-
lastSessionState = toReadySessionState(
|
|
532
|
-
for (const listener of stateListeners) {
|
|
533
|
-
listener(lastPluginSnapshot);
|
|
534
|
-
}
|
|
535
|
-
if (!previousSession || previousSession.controllingPlayerId !== lastSessionState.controllingPlayerId || previousSession.controllablePlayerIds.join("\0") !== lastSessionState.controllablePlayerIds.join("\0")) {
|
|
536
|
-
for (const listener of sessionListeners) {
|
|
537
|
-
listener(lastSessionState);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
542
|
+
lastSessionState = toReadySessionState(lastPluginFrame);
|
|
540
543
|
};
|
|
541
544
|
applyCurrentState();
|
|
542
545
|
const validate = async (playerId, interactionId, params = {}) => {
|
|
@@ -615,38 +618,16 @@ function createTestRuntime(options) {
|
|
|
615
618
|
interactionId
|
|
616
619
|
});
|
|
617
620
|
};
|
|
618
|
-
const setControllingPlayer = (playerId) => {
|
|
619
|
-
if (!playerIds.includes(playerId)) {
|
|
620
|
-
throw new Error(`Unknown controlling player '${playerId}'.`);
|
|
621
|
-
}
|
|
622
|
-
currentPlayerId = playerId;
|
|
623
|
-
applyCurrentState();
|
|
624
|
-
};
|
|
625
621
|
const runtime = {
|
|
626
|
-
validateInteraction: validate,
|
|
627
|
-
submitInteraction: submit,
|
|
622
|
+
validateInteraction: (interactionId, params) => validate(currentPlayerId, interactionId, params),
|
|
623
|
+
submitInteraction: (interactionId, params) => submit(currentPlayerId, interactionId, params),
|
|
628
624
|
getSessionState: () => lastSessionState,
|
|
629
|
-
disconnect: () => void 0
|
|
630
|
-
switchPlayer: (playerId) => {
|
|
631
|
-
setControllingPlayer(playerId);
|
|
632
|
-
},
|
|
633
|
-
getSnapshot: () => lastPluginSnapshot,
|
|
634
|
-
subscribeToState: (listener) => {
|
|
635
|
-
stateListeners.add(listener);
|
|
636
|
-
return () => {
|
|
637
|
-
stateListeners.delete(listener);
|
|
638
|
-
};
|
|
639
|
-
},
|
|
640
|
-
_subscribeToSessionState: (listener) => {
|
|
641
|
-
sessionListeners.add(listener);
|
|
642
|
-
return () => {
|
|
643
|
-
sessionListeners.delete(listener);
|
|
644
|
-
};
|
|
645
|
-
}
|
|
625
|
+
disconnect: () => void 0
|
|
646
626
|
};
|
|
647
627
|
return {
|
|
648
628
|
runtime,
|
|
649
|
-
|
|
629
|
+
getFrame: () => lastPluginFrame,
|
|
630
|
+
getSessionDescriptor: () => sessionDescriptor,
|
|
650
631
|
players: () => [...playerIds],
|
|
651
632
|
seat: (index) => {
|
|
652
633
|
if (!Number.isInteger(index) || index < 0 || index >= playerIds.length) {
|
|
@@ -670,8 +651,7 @@ function createTestRuntime(options) {
|
|
|
670
651
|
diagnosticEvents.length = 0;
|
|
671
652
|
lastDispatch = null;
|
|
672
653
|
}
|
|
673
|
-
}
|
|
674
|
-
setControllingPlayer
|
|
654
|
+
}
|
|
675
655
|
};
|
|
676
656
|
}
|
|
677
657
|
|
|
@@ -719,10 +699,22 @@ function digestUIFixtureJson(value) {
|
|
|
719
699
|
}
|
|
720
700
|
function digestUIFixtureRequest(request) {
|
|
721
701
|
return digestUIFixtureJson({
|
|
722
|
-
digestVersion: "ui-replay-request@
|
|
702
|
+
digestVersion: "ui-replay-request@2",
|
|
703
|
+
request
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
function digestUIFixtureTransportRequest(request) {
|
|
707
|
+
return digestUIFixtureJson({
|
|
708
|
+
digestVersion: "ui-fixture-runtime-command@2",
|
|
723
709
|
request
|
|
724
710
|
});
|
|
725
711
|
}
|
|
712
|
+
function digestPluginProtocolTape(tape) {
|
|
713
|
+
return digestUIFixtureJson({
|
|
714
|
+
digestVersion: "plugin-protocol-tape@2",
|
|
715
|
+
tape
|
|
716
|
+
});
|
|
717
|
+
}
|
|
726
718
|
function canonicalizeUIScenarioFixture(fixture) {
|
|
727
719
|
return canonicalizeUIFixtureJson({
|
|
728
720
|
...fixture,
|
|
@@ -744,11 +736,843 @@ function sha256(value) {
|
|
|
744
736
|
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
745
737
|
}
|
|
746
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;
|
|
1408
|
+
},
|
|
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")}`;
|
|
1557
|
+
};
|
|
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
|
+
|
|
747
1570
|
// src/testing/ui-fixture/schema.ts
|
|
748
1571
|
import { z } from "zod";
|
|
749
|
-
var UI_SCENARIO_FIXTURE_SCHEMA_VERSION =
|
|
750
|
-
var UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION =
|
|
751
|
-
var UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR =
|
|
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;
|
|
752
1576
|
var digestSchema = z.string().refine(isSha256Digest, {
|
|
753
1577
|
message: "Expected a sha256:<64 lowercase hex> digest."
|
|
754
1578
|
});
|
|
@@ -761,55 +1585,12 @@ var environmentSchema = z.object({
|
|
|
761
1585
|
timezone: z.literal("UTC"),
|
|
762
1586
|
viewportTags: z.array(z.enum(["desktop", "tablet", "phone", "touch"]))
|
|
763
1587
|
}).strict();
|
|
764
|
-
var
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
var gameplaySnapshotSchema = z.object({
|
|
771
|
-
currentPhase: nonEmptyStringSchema.nullable(),
|
|
772
|
-
currentStage: nonEmptyStringSchema.nullable(),
|
|
773
|
-
activePlayers: z.array(nonEmptyStringSchema),
|
|
774
|
-
simultaneousPhase: jsonSchema.nullable().optional(),
|
|
775
|
-
availableInteractions: z.array(jsonSchema),
|
|
776
|
-
zones: z.record(z.string(), jsonSchema)
|
|
777
|
-
}).catchall(jsonSchema);
|
|
778
|
-
var uiFixturePluginStateSnapshotSchema = z.object({
|
|
779
|
-
view: jsonSchema.nullable(),
|
|
780
|
-
gameplay: gameplaySnapshotSchema,
|
|
781
|
-
lobby: jsonSchema.nullable(),
|
|
782
|
-
notifications: z.array(jsonSchema),
|
|
783
|
-
session: pluginSessionSchema,
|
|
784
|
-
history: jsonSchema.nullable(),
|
|
785
|
-
syncId: z.number().int().nonnegative()
|
|
786
|
-
}).strict();
|
|
787
|
-
var uiFixtureFrameSchema = z.object({
|
|
788
|
-
id: nonEmptyStringSchema,
|
|
789
|
-
snapshot: uiFixturePluginStateSnapshotSchema,
|
|
790
|
-
projectionDigest: digestSchema
|
|
791
|
-
}).strict();
|
|
792
|
-
var uiFixtureTransportExchangeSchema = z.object({
|
|
793
|
-
id: nonEmptyStringSchema,
|
|
794
|
-
fromFrameId: nonEmptyStringSchema,
|
|
795
|
-
operation: z.enum(["validate", "submit", "refresh"]),
|
|
796
|
-
requestDigest: digestSchema,
|
|
797
|
-
response: z.union([
|
|
798
|
-
z.object({
|
|
799
|
-
kind: z.literal("accepted"),
|
|
800
|
-
nextFrameId: nonEmptyStringSchema
|
|
801
|
-
}).strict(),
|
|
802
|
-
z.object({
|
|
803
|
-
kind: z.literal("rejected"),
|
|
804
|
-
diagnostics: z.array(
|
|
805
|
-
z.object({
|
|
806
|
-
code: nonEmptyStringSchema,
|
|
807
|
-
message: nonEmptyStringSchema
|
|
808
|
-
}).strict()
|
|
809
|
-
)
|
|
810
|
-
}).strict()
|
|
811
|
-
])
|
|
812
|
-
}).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
|
+
);
|
|
813
1594
|
var browserInteractionIntentRequestSchema = z.object({
|
|
814
1595
|
surface: nonEmptyStringSchema,
|
|
815
1596
|
scopeId: nonEmptyStringSchema.optional(),
|
|
@@ -887,6 +1668,7 @@ var uiScenarioFixtureSchema = z.object({
|
|
|
887
1668
|
browserInteractionProtocol: z.literal(
|
|
888
1669
|
DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION
|
|
889
1670
|
),
|
|
1671
|
+
pluginRuntimeProtocol: z.literal(DREAMBOARD_PLUGIN_PROTOCOL_VERSION),
|
|
890
1672
|
id: nonEmptyStringSchema,
|
|
891
1673
|
title: nonEmptyStringSchema,
|
|
892
1674
|
gameId: nonEmptyStringSchema,
|
|
@@ -904,8 +1686,7 @@ var uiScenarioFixtureSchema = z.object({
|
|
|
904
1686
|
playerId: nonEmptyStringSchema.optional()
|
|
905
1687
|
}).strict(),
|
|
906
1688
|
environment: environmentSchema,
|
|
907
|
-
|
|
908
|
-
transport: z.array(uiFixtureTransportExchangeSchema),
|
|
1689
|
+
protocol: pluginProtocolTapeSchema,
|
|
909
1690
|
replay: z.array(uiScenarioReplayStepSchema),
|
|
910
1691
|
expected: z.object({
|
|
911
1692
|
initialProjectionDigest: digestSchema,
|
|
@@ -918,6 +1699,7 @@ var uiScenarioFixtureBundleIndexSchema = z.object({
|
|
|
918
1699
|
schemaVersion: z.literal(UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION),
|
|
919
1700
|
bundleId: nonEmptyStringSchema,
|
|
920
1701
|
sdkCommit: nonEmptyStringSchema,
|
|
1702
|
+
pluginRuntimeProtocol: z.literal(DREAMBOARD_PLUGIN_PROTOCOL_VERSION),
|
|
921
1703
|
browserInteractionProtocol: z.literal(
|
|
922
1704
|
DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION
|
|
923
1705
|
),
|
|
@@ -963,21 +1745,29 @@ function assertSupportedBrowserInteractionProtocol(version) {
|
|
|
963
1745
|
function validateUIScenarioFixtureReferences(fixture) {
|
|
964
1746
|
assertSupportedBrowserInteractionProtocol(fixture.browserInteractionProtocol);
|
|
965
1747
|
const frameIds = /* @__PURE__ */ new Set();
|
|
966
|
-
|
|
1748
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
1749
|
+
for (const frame of fixture.protocol.frames) {
|
|
967
1750
|
if (frameIds.has(frame.id)) {
|
|
968
1751
|
throw new Error(`Duplicate fixture frame id '${frame.id}'.`);
|
|
969
1752
|
}
|
|
970
1753
|
frameIds.add(frame.id);
|
|
971
1754
|
}
|
|
972
|
-
for (const
|
|
973
|
-
if (
|
|
974
|
-
throw new Error(
|
|
975
|
-
|
|
976
|
-
|
|
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;
|
|
977
1767
|
}
|
|
978
|
-
if (
|
|
1768
|
+
if (!frameIds.has(step.fromFrameId)) {
|
|
979
1769
|
throw new Error(
|
|
980
|
-
`
|
|
1770
|
+
`Protocol step '${step.id}' references missing fromFrameId '${step.fromFrameId}'.`
|
|
981
1771
|
);
|
|
982
1772
|
}
|
|
983
1773
|
}
|
|
@@ -1003,10 +1793,12 @@ function validateUIScenarioFixtureReferences(fixture) {
|
|
|
1003
1793
|
|
|
1004
1794
|
// src/testing/ui-fixture/compiler.ts
|
|
1005
1795
|
function compileUIScenarioFixture(options) {
|
|
1006
|
-
const firstFrame = options.frames[0];
|
|
1007
|
-
const finalFrame = options.
|
|
1796
|
+
const firstFrame = options.protocol.frames[0];
|
|
1797
|
+
const finalFrame = lastPublishedFrame(options.protocol);
|
|
1008
1798
|
if (!firstFrame || !finalFrame) {
|
|
1009
|
-
throw new Error(
|
|
1799
|
+
throw new Error(
|
|
1800
|
+
"compileUIScenarioFixture requires at least one protocol frame."
|
|
1801
|
+
);
|
|
1010
1802
|
}
|
|
1011
1803
|
const replay = options.replay ?? [];
|
|
1012
1804
|
const submissionDigest = options.expected?.submissionDigest ?? digestUIFixtureJson({
|
|
@@ -1024,8 +1816,9 @@ function compileUIScenarioFixture(options) {
|
|
|
1024
1816
|
});
|
|
1025
1817
|
return parseUIScenarioFixture(
|
|
1026
1818
|
canonicalizeUIScenarioFixture({
|
|
1027
|
-
schemaVersion:
|
|
1819
|
+
schemaVersion: 2,
|
|
1028
1820
|
browserInteractionProtocol: DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION,
|
|
1821
|
+
pluginRuntimeProtocol: DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
|
|
1029
1822
|
id: options.id,
|
|
1030
1823
|
title: options.title,
|
|
1031
1824
|
gameId: options.gameId,
|
|
@@ -1033,8 +1826,7 @@ function compileUIScenarioFixture(options) {
|
|
|
1033
1826
|
source: options.source,
|
|
1034
1827
|
viewer: options.viewer,
|
|
1035
1828
|
environment: options.environment,
|
|
1036
|
-
|
|
1037
|
-
transport: [...options.transport ?? []],
|
|
1829
|
+
protocol: options.protocol,
|
|
1038
1830
|
replay,
|
|
1039
1831
|
expected: {
|
|
1040
1832
|
initialProjectionDigest: options.expected?.initialProjectionDigest ?? firstFrame.projectionDigest,
|
|
@@ -1045,6 +1837,16 @@ function compileUIScenarioFixture(options) {
|
|
|
1045
1837
|
})
|
|
1046
1838
|
);
|
|
1047
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
|
+
}
|
|
1048
1850
|
function assertDeterministicUIScenarioFixture(first, second) {
|
|
1049
1851
|
const firstBytes = serializeUIScenarioFixture(first);
|
|
1050
1852
|
const secondBytes = serializeUIScenarioFixture(second);
|
|
@@ -1118,8 +1920,10 @@ function assertUIStepExpectationSatisfied(stepId, expected, actual) {
|
|
|
1118
1920
|
}
|
|
1119
1921
|
}
|
|
1120
1922
|
export {
|
|
1923
|
+
FixturePluginRuntime,
|
|
1121
1924
|
StaleContractArtifactError,
|
|
1122
1925
|
UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION,
|
|
1926
|
+
UI_SCENARIO_FIXTURE_PLUGIN_RUNTIME_PROTOCOL,
|
|
1123
1927
|
UI_SCENARIO_FIXTURE_SCHEMA_VERSION,
|
|
1124
1928
|
UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR,
|
|
1125
1929
|
assertDeterministicUIScenarioFixture,
|
|
@@ -1129,23 +1933,34 @@ export {
|
|
|
1129
1933
|
canonicalUIFixtureJson,
|
|
1130
1934
|
canonicalizeUIFixtureJson,
|
|
1131
1935
|
canonicalizeUIScenarioFixture,
|
|
1936
|
+
compareUIParityObservations,
|
|
1937
|
+
compilePluginProtocolTape,
|
|
1132
1938
|
compileUIScenarioFixture,
|
|
1939
|
+
createDeterministicIdFactory,
|
|
1133
1940
|
createExpectApi,
|
|
1941
|
+
createFixtureHostHarness,
|
|
1942
|
+
createReducerScenarioRunner,
|
|
1134
1943
|
createTestRuntime,
|
|
1944
|
+
createUIParityObservationFromFixture,
|
|
1945
|
+
defaultFixtureEnvironmentInit,
|
|
1135
1946
|
defineBase,
|
|
1136
1947
|
defineScenario,
|
|
1948
|
+
digestPluginProtocolTape,
|
|
1137
1949
|
digestUIFixtureJson,
|
|
1138
1950
|
digestUIFixtureRequest,
|
|
1951
|
+
digestUIFixtureTransportRequest,
|
|
1139
1952
|
digestUIScenarioFixture,
|
|
1953
|
+
fixtureEnvironmentInitFor,
|
|
1140
1954
|
isSha256Digest,
|
|
1141
1955
|
isStaleContractArtifactError,
|
|
1956
|
+
parseUIParityObservationV1,
|
|
1142
1957
|
parseUIScenarioFixture,
|
|
1143
1958
|
parseUIScenarioFixtureBundleIndex,
|
|
1959
|
+
pluginProtocolTapeSchema,
|
|
1144
1960
|
portableSemanticReplayStepSchema,
|
|
1145
1961
|
serializeUIScenarioFixture,
|
|
1146
1962
|
uiFixtureFrameSchema,
|
|
1147
|
-
|
|
1148
|
-
uiFixtureTransportExchangeSchema,
|
|
1963
|
+
uiFixtureProtocolStepSchema,
|
|
1149
1964
|
uiReplayExecutionSchema,
|
|
1150
1965
|
uiReplayRequestSchema,
|
|
1151
1966
|
uiResolvedReplayIdentitySchema,
|