@seriousgaming/sdk 0.0.0-stage → 1.0.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/LICENSE +202 -0
- package/NOTICE +1 -0
- package/README.md +33 -2
- package/lib/build/callbacks.d.ts +998 -0
- package/lib/build/callbacks.js +32 -0
- package/lib/build/context-merge.d.ts +59 -0
- package/lib/build/context-merge.js +118 -0
- package/lib/build/document.d.ts +36 -0
- package/lib/build/document.js +8 -0
- package/lib/build/encoding.d.ts +26 -0
- package/lib/build/encoding.js +78 -0
- package/lib/build/endpoints.d.ts +421 -0
- package/lib/build/endpoints.js +46 -0
- package/lib/build/hooks.d.ts +74 -0
- package/lib/build/hooks.js +1 -0
- package/lib/build/index.d.ts +18 -0
- package/lib/build/index.js +16 -0
- package/lib/build/llm.d.ts +166 -0
- package/lib/build/llm.js +17 -0
- package/lib/build/resource-schema.d.ts +1048 -0
- package/lib/build/resource-schema.js +7 -0
- package/lib/build/scheduling.d.ts +112 -0
- package/lib/build/scheduling.js +217 -0
- package/lib/build/seed.d.ts +51 -0
- package/lib/build/seed.js +89 -0
- package/lib/build/signatures.d.ts +372 -0
- package/lib/build/signatures.js +1 -0
- package/lib/build/speech.d.ts +102 -0
- package/lib/build/speech.js +11 -0
- package/lib/build/tick-validation.d.ts +23 -0
- package/lib/build/tick-validation.js +96 -0
- package/lib/build/timer.d.ts +74 -0
- package/lib/build/timer.js +1 -0
- package/lib/build/validators.d.ts +116 -0
- package/lib/build/validators.js +1 -0
- package/lib/commands.d.ts +357 -0
- package/lib/commands.js +14 -0
- package/lib/e2e.d.ts +238 -0
- package/lib/e2e.js +248 -0
- package/lib/projection.d.ts +266 -0
- package/lib/projection.js +23 -0
- package/lib/testing/index.d.ts +13 -0
- package/lib/testing/index.js +13 -0
- package/lib/testing/logic-args.d.ts +177 -0
- package/lib/testing/logic-args.js +229 -0
- package/lib/testing/stub-services.d.ts +41 -0
- package/lib/testing/stub-services.js +176 -0
- package/lib/ui/CrossTeamDataContext.d.ts +36 -0
- package/lib/ui/CrossTeamDataContext.js +32 -0
- package/lib/ui/PresentationContext.d.ts +72 -0
- package/lib/ui/PresentationContext.js +13 -0
- package/lib/ui/PresentationWrapper.d.ts +12 -0
- package/lib/ui/PresentationWrapper.js +78 -0
- package/lib/ui/SessionUserInterfaceCallbacksContext.d.ts +3 -0
- package/lib/ui/SessionUserInterfaceCallbacksContext.js +7 -0
- package/lib/ui/SessionUserInterfaceDataContext.d.ts +103 -0
- package/lib/ui/SessionUserInterfaceDataContext.js +17 -0
- package/lib/ui/SessionUserInterfaceHookFailuresContext.d.ts +44 -0
- package/lib/ui/SessionUserInterfaceHookFailuresContext.js +15 -0
- package/lib/ui/SessionUserInterfaceHostOverlaysContext.d.ts +23 -0
- package/lib/ui/SessionUserInterfaceHostOverlaysContext.js +23 -0
- package/lib/ui/SessionUserInterfaceLocaleContext.d.ts +11 -0
- package/lib/ui/SessionUserInterfaceLocaleContext.js +13 -0
- package/lib/ui/SessionUserInterfaceMetaContext.d.ts +39 -0
- package/lib/ui/SessionUserInterfaceMetaContext.js +13 -0
- package/lib/ui/SessionUserInterfaceParticipantsContext.d.ts +12 -0
- package/lib/ui/SessionUserInterfaceParticipantsContext.js +16 -0
- package/lib/ui/SessionUserInterfaceTeamConfigurationContext.d.ts +53 -0
- package/lib/ui/SessionUserInterfaceTeamConfigurationContext.js +23 -0
- package/lib/ui/SessionUserInterfaceTeamLinksContext.d.ts +56 -0
- package/lib/ui/SessionUserInterfaceTeamLinksContext.js +12 -0
- package/lib/ui/SessionUserInterfaceTeamsContext.d.ts +13 -0
- package/lib/ui/SessionUserInterfaceTeamsContext.js +13 -0
- package/lib/ui/SessionUserInterfaceThemeContext.d.ts +9 -0
- package/lib/ui/SessionUserInterfaceThemeContext.js +7 -0
- package/lib/ui/SessionUserInterfaceThemePreferenceContext.d.ts +3 -0
- package/lib/ui/SessionUserInterfaceThemePreferenceContext.js +7 -0
- package/lib/ui/SessionUserInterfaceWalkthroughLinkContext.d.ts +42 -0
- package/lib/ui/SessionUserInterfaceWalkthroughLinkContext.js +26 -0
- package/lib/ui/SessionUserInterfaceWrapper.d.ts +8 -0
- package/lib/ui/SessionUserInterfaceWrapper.js +321 -0
- package/lib/ui/SettingsEditorCallbacksContext.d.ts +3 -0
- package/lib/ui/SettingsEditorCallbacksContext.js +7 -0
- package/lib/ui/SettingsEditorContext.d.ts +33 -0
- package/lib/ui/SettingsEditorContext.js +16 -0
- package/lib/ui/SettingsEditorLocaleContext.d.ts +13 -0
- package/lib/ui/SettingsEditorLocaleContext.js +15 -0
- package/lib/ui/SettingsEditorThemeContext.d.ts +9 -0
- package/lib/ui/SettingsEditorThemeContext.js +7 -0
- package/lib/ui/SettingsEditorThemePreferenceContext.d.ts +3 -0
- package/lib/ui/SettingsEditorThemePreferenceContext.js +7 -0
- package/lib/ui/SettingsEditorWrapper.d.ts +13 -0
- package/lib/ui/SettingsEditorWrapper.js +74 -0
- package/lib/ui/UserInterfaceCallbacksContext.d.ts +3 -0
- package/lib/ui/UserInterfaceCallbacksContext.js +7 -0
- package/lib/ui/UserInterfaceDataContext.d.ts +78 -0
- package/lib/ui/UserInterfaceDataContext.js +11 -0
- package/lib/ui/UserInterfaceLocaleContext.d.ts +13 -0
- package/lib/ui/UserInterfaceLocaleContext.js +15 -0
- package/lib/ui/UserInterfaceOwnTeamContext.d.ts +25 -0
- package/lib/ui/UserInterfaceOwnTeamContext.js +26 -0
- package/lib/ui/UserInterfaceOwnTeamJoinLinkContext.d.ts +52 -0
- package/lib/ui/UserInterfaceOwnTeamJoinLinkContext.js +13 -0
- package/lib/ui/UserInterfaceParticipantContext.d.ts +16 -0
- package/lib/ui/UserInterfaceParticipantContext.js +24 -0
- package/lib/ui/UserInterfaceParticipantsContext.d.ts +12 -0
- package/lib/ui/UserInterfaceParticipantsContext.js +13 -0
- package/lib/ui/UserInterfaceRunKindContext.d.ts +31 -0
- package/lib/ui/UserInterfaceRunKindContext.js +31 -0
- package/lib/ui/UserInterfaceTeamsContext.d.ts +12 -0
- package/lib/ui/UserInterfaceTeamsContext.js +13 -0
- package/lib/ui/UserInterfaceThemeContext.d.ts +9 -0
- package/lib/ui/UserInterfaceThemeContext.js +7 -0
- package/lib/ui/UserInterfaceThemePreferenceContext.d.ts +3 -0
- package/lib/ui/UserInterfaceThemePreferenceContext.js +7 -0
- package/lib/ui/UserInterfaceTimerContext.d.ts +10 -0
- package/lib/ui/UserInterfaceTimerContext.js +12 -0
- package/lib/ui/UserInterfaceValidationContext.d.ts +25 -0
- package/lib/ui/UserInterfaceValidationContext.js +14 -0
- package/lib/ui/UserInterfaceViewerRoleContext.d.ts +14 -0
- package/lib/ui/UserInterfaceViewerRoleContext.js +17 -0
- package/lib/ui/UserInterfaceWrapper.d.ts +8 -0
- package/lib/ui/UserInterfaceWrapper.js +219 -0
- package/lib/ui/expectedParentOrigin.d.ts +10 -0
- package/lib/ui/expectedParentOrigin.js +32 -0
- package/lib/ui/index.d.ts +50 -0
- package/lib/ui/index.js +52 -0
- package/lib/ui/internal/SurfaceGate.d.ts +19 -0
- package/lib/ui/internal/SurfaceGate.js +17 -0
- package/lib/ui/internal/SurfaceScope.d.ts +17 -0
- package/lib/ui/internal/SurfaceScope.js +16 -0
- package/lib/ui/internal/createContexts.d.ts +50 -0
- package/lib/ui/internal/createContexts.js +32 -0
- package/lib/ui/internal/document-attributes.d.ts +9 -0
- package/lib/ui/internal/document-attributes.js +87 -0
- package/lib/ui/internal/surface-timer.d.ts +3 -0
- package/lib/ui/internal/surface-timer.js +11 -0
- package/lib/ui/internal/useLatestRef.d.ts +7 -0
- package/lib/ui/internal/useLatestRef.js +11 -0
- package/lib/ui/internal/useOptimisticMerge.d.ts +19 -0
- package/lib/ui/internal/useOptimisticMerge.js +28 -0
- package/lib/ui/internal/useSurfaceControls.d.ts +52 -0
- package/lib/ui/internal/useSurfaceControls.js +19 -0
- package/lib/ui/internal/useTypedPastState.d.ts +17 -0
- package/lib/ui/internal/useTypedPastState.js +17 -0
- package/lib/ui/mutation-commit.d.ts +32 -0
- package/lib/ui/mutation-commit.js +1 -0
- package/lib/ui/mutation-outcome.d.ts +66 -0
- package/lib/ui/mutation-outcome.js +49 -0
- package/lib/ui/request-id.d.ts +18 -0
- package/lib/ui/request-id.js +44 -0
- package/lib/ui/testing.d.ts +365 -0
- package/lib/ui/testing.js +572 -0
- package/lib/ui/tick-snapshot.d.ts +28 -0
- package/lib/ui/tick-snapshot.js +34 -0
- package/lib/ui/ui-protocol.d.ts +799 -0
- package/lib/ui/ui-protocol.js +50 -0
- package/lib/ui/useAsset.d.ts +80 -0
- package/lib/ui/useAsset.js +139 -0
- package/lib/ui/useEndpointCallbacks.d.ts +31 -0
- package/lib/ui/useEndpointCallbacks.js +68 -0
- package/lib/ui/useHostChannel.d.ts +90 -0
- package/lib/ui/useHostChannel.js +338 -0
- package/lib/ui/useMutationReporter.d.ts +100 -0
- package/lib/ui/useMutationReporter.js +97 -0
- package/lib/ui/usePastState.d.ts +39 -0
- package/lib/ui/usePastState.js +63 -0
- package/lib/ui/useViewerStorage.d.ts +54 -0
- package/lib/ui/useViewerStorage.js +141 -0
- package/package.json +89 -3
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { createElement } from 'react';
|
|
3
|
+
import { applyContextUpdate } from '../build/context-merge.js';
|
|
4
|
+
import { DEFAULT_SURFACE_TIMER } from './internal/surface-timer.js';
|
|
5
|
+
import { SessionUserInterfaceCallbacksContext } from './SessionUserInterfaceCallbacksContext.js';
|
|
6
|
+
import { SessionUserInterfaceDataContext } from './SessionUserInterfaceDataContext.js';
|
|
7
|
+
import { SessionUserInterfaceHookFailuresContext } from './SessionUserInterfaceHookFailuresContext.js';
|
|
8
|
+
import { SessionUserInterfaceHostOverlaysContext } from './SessionUserInterfaceHostOverlaysContext.js';
|
|
9
|
+
import { SessionUserInterfaceLocaleContext } from './SessionUserInterfaceLocaleContext.js';
|
|
10
|
+
import { SessionUserInterfaceMetaContext } from './SessionUserInterfaceMetaContext.js';
|
|
11
|
+
import { SessionUserInterfaceParticipantsContext } from './SessionUserInterfaceParticipantsContext.js';
|
|
12
|
+
import { SessionUserInterfaceTeamConfigurationContext } from './SessionUserInterfaceTeamConfigurationContext.js';
|
|
13
|
+
import { SessionUserInterfaceTeamLinksContext } from './SessionUserInterfaceTeamLinksContext.js';
|
|
14
|
+
import { SessionUserInterfaceTeamsContext } from './SessionUserInterfaceTeamsContext.js';
|
|
15
|
+
import { SessionUserInterfaceThemeContext } from './SessionUserInterfaceThemeContext.js';
|
|
16
|
+
import { SessionUserInterfaceThemePreferenceContext } from './SessionUserInterfaceThemePreferenceContext.js';
|
|
17
|
+
import { SessionUserInterfaceWalkthroughLinkContext } from './SessionUserInterfaceWalkthroughLinkContext.js';
|
|
18
|
+
import { SettingsEditorCallbacksContext } from './SettingsEditorCallbacksContext.js';
|
|
19
|
+
import { SettingsEditorContext } from './SettingsEditorContext.js';
|
|
20
|
+
import { SettingsEditorLocaleContext } from './SettingsEditorLocaleContext.js';
|
|
21
|
+
import { SettingsEditorThemeContext } from './SettingsEditorThemeContext.js';
|
|
22
|
+
import { SettingsEditorThemePreferenceContext } from './SettingsEditorThemePreferenceContext.js';
|
|
23
|
+
import { AssetContext } from './useAsset.js';
|
|
24
|
+
import { PastStateContext } from './usePastState.js';
|
|
25
|
+
import { UserInterfaceCallbacksContext } from './UserInterfaceCallbacksContext.js';
|
|
26
|
+
import { UserInterfaceDataContext } from './UserInterfaceDataContext.js';
|
|
27
|
+
import { UserInterfaceLocaleContext } from './UserInterfaceLocaleContext.js';
|
|
28
|
+
import { UserInterfaceOwnTeamContext } from './UserInterfaceOwnTeamContext.js';
|
|
29
|
+
import { UserInterfaceOwnTeamJoinLinkContext, } from './UserInterfaceOwnTeamJoinLinkContext.js';
|
|
30
|
+
import { UserInterfaceParticipantContext } from './UserInterfaceParticipantContext.js';
|
|
31
|
+
import { UserInterfaceParticipantsContext } from './UserInterfaceParticipantsContext.js';
|
|
32
|
+
import { UserInterfaceRunKindContext } from './UserInterfaceRunKindContext.js';
|
|
33
|
+
import { UserInterfaceTeamsContext } from './UserInterfaceTeamsContext.js';
|
|
34
|
+
import { UserInterfaceThemeContext } from './UserInterfaceThemeContext.js';
|
|
35
|
+
import { UserInterfaceThemePreferenceContext } from './UserInterfaceThemePreferenceContext.js';
|
|
36
|
+
import { UserInterfaceTimerContext } from './UserInterfaceTimerContext.js';
|
|
37
|
+
import { UserInterfaceValidationContext } from './UserInterfaceValidationContext.js';
|
|
38
|
+
import { UserInterfaceViewerRoleContext } from './UserInterfaceViewerRoleContext.js';
|
|
39
|
+
import { ViewerStorageContext } from './useViewerStorage.js';
|
|
40
|
+
// Each `contrastText` is the black or white a host would pick (§12.3): the one
|
|
41
|
+
// of the two contrasting more with its colour, at 4.6:1 or better throughout.
|
|
42
|
+
const DEFAULT_LIGHT_COLORS = {
|
|
43
|
+
primary: '#1976d2',
|
|
44
|
+
secondary: '#9c27b0',
|
|
45
|
+
success: '#4caf50',
|
|
46
|
+
error: '#f44336',
|
|
47
|
+
warning: '#ff9800',
|
|
48
|
+
info: '#2196f3',
|
|
49
|
+
contrastText: {
|
|
50
|
+
primary: '#ffffff',
|
|
51
|
+
secondary: '#ffffff',
|
|
52
|
+
success: '#000000',
|
|
53
|
+
error: '#000000',
|
|
54
|
+
warning: '#000000',
|
|
55
|
+
info: '#000000',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const DEFAULT_DARK_COLORS = {
|
|
59
|
+
primary: '#90caf9',
|
|
60
|
+
secondary: '#ce93d8',
|
|
61
|
+
success: '#66bb6a',
|
|
62
|
+
error: '#f44336',
|
|
63
|
+
warning: '#ffa726',
|
|
64
|
+
info: '#29b6f6',
|
|
65
|
+
contrastText: {
|
|
66
|
+
primary: '#000000',
|
|
67
|
+
secondary: '#000000',
|
|
68
|
+
success: '#000000',
|
|
69
|
+
error: '#000000',
|
|
70
|
+
warning: '#000000',
|
|
71
|
+
info: '#000000',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
/** What `useViewerStorage` namespaces by in a render with no host. */
|
|
75
|
+
const DEFAULT_SESSION_KEY = 'test-session-key';
|
|
76
|
+
/**
|
|
77
|
+
* Creates a valid UserInterfaceTheme for use in tests.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* // Basic usage — light mode with default MUI palette
|
|
81
|
+
* <UserInterfaceThemeContext.Provider value={createMockTheme()}>
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // Override mode or colours
|
|
85
|
+
* <UserInterfaceThemeContext.Provider value={createMockTheme({ mode: 'dark' })}>
|
|
86
|
+
*/
|
|
87
|
+
export function createMockTheme(overrides) {
|
|
88
|
+
return {
|
|
89
|
+
mode: 'light',
|
|
90
|
+
light: DEFAULT_LIGHT_COLORS,
|
|
91
|
+
dark: DEFAULT_DARK_COLORS,
|
|
92
|
+
...overrides,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Creates a valid SettingsEditorTheme for use in tests.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* <SettingsEditorThemeContext.Provider value={createMockSettingsEditorTheme()}>
|
|
100
|
+
*/
|
|
101
|
+
export function createMockSettingsEditorTheme(overrides) {
|
|
102
|
+
return {
|
|
103
|
+
mode: 'light',
|
|
104
|
+
light: DEFAULT_LIGHT_COLORS,
|
|
105
|
+
dark: DEFAULT_DARK_COLORS,
|
|
106
|
+
...overrides,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Creates a valid SessionUserInterfaceTheme for use in tests.
|
|
111
|
+
*/
|
|
112
|
+
export function createMockSessionTheme(overrides) {
|
|
113
|
+
return {
|
|
114
|
+
mode: 'light',
|
|
115
|
+
light: DEFAULT_LIGHT_COLORS,
|
|
116
|
+
dark: DEFAULT_DARK_COLORS,
|
|
117
|
+
...overrides,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Stub factory — creates a no-op function (or async no-op) suitable for use
|
|
122
|
+
// as a default callback in test helpers. Consumers can override individual
|
|
123
|
+
// callbacks via the options object.
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
const noop = () => { };
|
|
126
|
+
const asyncNoop = async () => { };
|
|
127
|
+
// Default mock for `getPastState`, which answers with what the query reaches
|
|
128
|
+
// rather than with a nullable value: an empty result is 'nothing reviewable',
|
|
129
|
+
// so a surface under test renders its empty state rather than having to guard
|
|
130
|
+
// a null. Tests that exercise review screens override it via `callbacks`.
|
|
131
|
+
const asyncEmpty = async () => [];
|
|
132
|
+
const asyncEmptyReference = async () => '';
|
|
133
|
+
const asyncEmptyAsset = async () => ({
|
|
134
|
+
data: new Uint8Array(),
|
|
135
|
+
mimeType: 'application/octet-stream',
|
|
136
|
+
name: '',
|
|
137
|
+
});
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Default values reused across render helpers
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
const DEFAULT_LOCALE = { locale: 'en' };
|
|
142
|
+
// Sentinel prefixes for test helpers. The wrappers themselves no longer carry
|
|
143
|
+
// a default — production hosts must declare prefixes via setAssetUrls. Tests
|
|
144
|
+
// only need *some* prefix so that `getAssetUrl` returns a deterministic value;
|
|
145
|
+
// override via the `blobPrefix` / `scenarioAssetPrefix` / `sharedAssetPrefix`
|
|
146
|
+
// options.
|
|
147
|
+
const DEFAULT_BLOB_URL_PREFIX = 'test:blob:';
|
|
148
|
+
const DEFAULT_ASSET_URL_PREFIX = 'test:asset:';
|
|
149
|
+
const DEFAULT_SHARED_ASSET_URL_PREFIX = 'test:shared:';
|
|
150
|
+
// A live, unfrozen, PIN-less session with the waiting room closed: the state a
|
|
151
|
+
// facilitator surface spends nearly all its time in, so tests opt in to the
|
|
152
|
+
// awkward cases rather than out of them.
|
|
153
|
+
const DEFAULT_SESSION_META = {
|
|
154
|
+
sessionId: 'session-1',
|
|
155
|
+
name: 'Test session',
|
|
156
|
+
readOnly: false,
|
|
157
|
+
waitingRoomOpen: false,
|
|
158
|
+
joinPin: null,
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Bounds a rendered surface sees when a test does not supply its own.
|
|
162
|
+
*
|
|
163
|
+
* Unlike the wrapper's pre-handshake default, this stands in for bounds the
|
|
164
|
+
* host has already sent, so it is a plausible session rather than the
|
|
165
|
+
* unreachable `max: 0` (#365). A test asserting on behaviour at the maximum
|
|
166
|
+
* must pass its own `teamConfiguration`: the helper's number is a convenience,
|
|
167
|
+
* never the thing under test.
|
|
168
|
+
*/
|
|
169
|
+
const DEFAULT_TEAM_CONFIGURATION = {
|
|
170
|
+
mode: 'independent',
|
|
171
|
+
max: 8,
|
|
172
|
+
};
|
|
173
|
+
const BLOB_PREFIX = 'blob:';
|
|
174
|
+
const ASSET_PREFIX = 'asset:';
|
|
175
|
+
const SHARED_PREFIX = 'shared:';
|
|
176
|
+
/**
|
|
177
|
+
* Construct a `getAssetUrl` stub that mirrors the SDK wrappers' production
|
|
178
|
+
* behaviour: dispatch on the reference prefix and concatenate with the
|
|
179
|
+
* matching URL prefix. Used by every render helper below.
|
|
180
|
+
*/
|
|
181
|
+
function makeGetAssetUrl(blobPrefix, scenarioAssetPrefix, sharedAssetPrefix) {
|
|
182
|
+
return (reference) => {
|
|
183
|
+
if (reference.startsWith(BLOB_PREFIX)) {
|
|
184
|
+
return blobPrefix + reference.slice(BLOB_PREFIX.length);
|
|
185
|
+
}
|
|
186
|
+
if (reference.startsWith(SHARED_PREFIX)) {
|
|
187
|
+
return sharedAssetPrefix + reference.slice(SHARED_PREFIX.length);
|
|
188
|
+
}
|
|
189
|
+
if (reference.startsWith(ASSET_PREFIX)) {
|
|
190
|
+
return scenarioAssetPrefix + reference.slice(ASSET_PREFIX.length);
|
|
191
|
+
}
|
|
192
|
+
throw new Error(`Invalid asset reference "${reference}": expected an "asset:", "shared:", or "blob:" prefix`);
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Creates a valid Participant for use in tests — the caller's own
|
|
197
|
+
* identity, as returned by `useUserInterfaceParticipant()`.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* // The caller, with a custom name
|
|
201
|
+
* renderUserInterface(<App />, { participant: createMockParticipant({ name: 'Alice' }) });
|
|
202
|
+
*/
|
|
203
|
+
export function createMockParticipant(overrides) {
|
|
204
|
+
return {
|
|
205
|
+
id: '1',
|
|
206
|
+
name: 'Participant 1',
|
|
207
|
+
index: 0,
|
|
208
|
+
connected: true,
|
|
209
|
+
teamId: 'team-1',
|
|
210
|
+
...overrides,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Creates a roster of participants for use in tests — the list returned by
|
|
215
|
+
* `useUserInterfaceParticipants()` (and the session equivalent, which
|
|
216
|
+
* shares the same shape). Ids, names, and indices follow the runtime's
|
|
217
|
+
* conventions (`id` is `String(index + 1)`, `name` is `Participant N`), and
|
|
218
|
+
* every entry is `connected` by default.
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* // A team of three, all online
|
|
222
|
+
* renderUserInterface(<App />, { participants: createMockParticipants(3) });
|
|
223
|
+
*/
|
|
224
|
+
export function createMockParticipants(count = 1, teamId = 'team-1') {
|
|
225
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
226
|
+
id: String(index + 1),
|
|
227
|
+
name: `Participant ${index + 1}`,
|
|
228
|
+
index,
|
|
229
|
+
connected: true,
|
|
230
|
+
teamId,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* A `TeamState` for a session-surface fixture, with the fields a test rarely
|
|
235
|
+
* cares about filled in.
|
|
236
|
+
*
|
|
237
|
+
* `revision` is the reason this exists. It is required, because the wrapper
|
|
238
|
+
* needs a baseline to compare an `updateTeamState` delta against, but no
|
|
239
|
+
* surface ever reads it, so making every fixture carry one would be friction
|
|
240
|
+
* for a field with no meaning to the test.
|
|
241
|
+
*
|
|
242
|
+
* Type it with the surface's generated `TeamContext`, and a shared capability
|
|
243
|
+
* left on a team's fixture fails to compile, as reading one through a team
|
|
244
|
+
* does in the surface. Capabilities the fixture leaves out are absent.
|
|
245
|
+
*
|
|
246
|
+
* ```typescript
|
|
247
|
+
* import type { TeamContext } from '../types';
|
|
248
|
+
*
|
|
249
|
+
* createMockTeamState<TeamContext>({ context: { price: { Price: 5 } } });
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
export function createMockTeamState(overrides) {
|
|
253
|
+
const { context, ...rest } = overrides ?? {};
|
|
254
|
+
return {
|
|
255
|
+
context: (context ?? {}),
|
|
256
|
+
completedTicks: 0,
|
|
257
|
+
timer: DEFAULT_SURFACE_TIMER,
|
|
258
|
+
revision: 1,
|
|
259
|
+
...rest,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* A `SharedState` for a session-surface fixture: the shared store's context,
|
|
264
|
+
* with a `revision` filled in for the same reason {@link createMockTeamState}
|
|
265
|
+
* fills one. Type it with the surface's generated `SharedContext`.
|
|
266
|
+
*
|
|
267
|
+
* ```typescript
|
|
268
|
+
* createMockSharedState<SharedContext>({ context: { market: { Price: 5 } } });
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
export function createMockSharedState(overrides) {
|
|
272
|
+
const { context, ...rest } = overrides ?? {};
|
|
273
|
+
return {
|
|
274
|
+
context: (context ?? {}),
|
|
275
|
+
revision: 1,
|
|
276
|
+
...rest,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* A `setData` for a stateful test harness that writes the way the host does:
|
|
281
|
+
* objects merge recursively, arrays, scalars and `null` replace whole, and
|
|
282
|
+
* any `deletes` are removed after the merge.
|
|
283
|
+
*
|
|
284
|
+
* A hand-rolled `{ ...previous, ...patch }` does not. It replaces a whole
|
|
285
|
+
* capability with whatever the patch names, so a minimal write (the shape
|
|
286
|
+
* every concurrency-safe surface sends) appears to wipe every field it did
|
|
287
|
+
* not mention. Tests then fail on writes that are correct in production, or
|
|
288
|
+
* worse, pass on writes that are not (#520).
|
|
289
|
+
*
|
|
290
|
+
* ```tsx
|
|
291
|
+
* const [data, setLocal] = useState(mockData);
|
|
292
|
+
* const setData = mergingSetData(setLocal);
|
|
293
|
+
* setData({}, { deletes: [`pinboard.Notes.${noteId}`] });
|
|
294
|
+
* ```
|
|
295
|
+
*
|
|
296
|
+
* Wrap it in a `vi.fn()` when the test also asserts on the payload.
|
|
297
|
+
*/
|
|
298
|
+
export function mergingSetData(setLocal) {
|
|
299
|
+
// `T extends object` rather than `Record<string, unknown>`: a generated
|
|
300
|
+
// `Context` is an interface, and an interface has no implicit index
|
|
301
|
+
// signature, so the tighter constraint would reject the one type every
|
|
302
|
+
// caller actually holds. The merge only ever reads own enumerable keys.
|
|
303
|
+
return (patch, options) => setLocal((previous) => applyContextUpdate(previous, patch, options?.deletes));
|
|
304
|
+
}
|
|
305
|
+
const DEFAULT_PARTICIPANT = createMockParticipant();
|
|
306
|
+
const DEFAULT_TIMER = DEFAULT_SURFACE_TIMER;
|
|
307
|
+
/**
|
|
308
|
+
* Resolve the own team a rendered player surface should see, mirroring the
|
|
309
|
+
* fallback order the real wrapper applies: an explicit value wins, otherwise the
|
|
310
|
+
* participant's team is resolved against the team list. An observer render (no
|
|
311
|
+
* participant) yields `null` unless `ownTeam` is given, matching a host that does
|
|
312
|
+
* not send `setOwnTeam`.
|
|
313
|
+
*/
|
|
314
|
+
function resolveOwnTeam(options) {
|
|
315
|
+
if (options.ownTeam !== undefined)
|
|
316
|
+
return options.ownTeam;
|
|
317
|
+
const participant = options.participant !== undefined
|
|
318
|
+
? options.participant
|
|
319
|
+
: options.viewerRole === 'observer'
|
|
320
|
+
? null
|
|
321
|
+
: DEFAULT_PARTICIPANT;
|
|
322
|
+
if (!participant)
|
|
323
|
+
return null;
|
|
324
|
+
return (options.teams ?? []).find((team) => team.teamId === participant.teamId) ?? null;
|
|
325
|
+
}
|
|
326
|
+
function provide(context, value) {
|
|
327
|
+
return (child) => createElement(context.Provider, { value }, child);
|
|
328
|
+
}
|
|
329
|
+
function wrapProviders(providers, child) {
|
|
330
|
+
return providers.reduceRight((acc, wrap) => wrap(acc), child);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Renders a component wrapped in all UserInterface context providers with
|
|
334
|
+
* sensible defaults. Override any context value via the options object.
|
|
335
|
+
*
|
|
336
|
+
* Returns the standard `@testing-library/react` render result plus the mock
|
|
337
|
+
* `setData` and resolved `callbacks` for assertions.
|
|
338
|
+
*
|
|
339
|
+
* Callbacks you do not supply default to no-ops rather than spies, so pass your
|
|
340
|
+
* own `vi.fn()` for any you intend to assert on.
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ```tsx
|
|
344
|
+
* const requestTick = vi.fn();
|
|
345
|
+
* const { setData } = renderUserInterface(<App />, {
|
|
346
|
+
* data: { 'com.example.cap': { Score: 42 } },
|
|
347
|
+
* settings: { scoring: { MaxScore: 100 } },
|
|
348
|
+
* callbacks: { requestTick },
|
|
349
|
+
* });
|
|
350
|
+
*
|
|
351
|
+
* fireEvent.click(screen.getByText('Submit'));
|
|
352
|
+
* expect(requestTick).toHaveBeenCalled();
|
|
353
|
+
* ```
|
|
354
|
+
*/
|
|
355
|
+
export function renderUserInterface(ui, options = {}) {
|
|
356
|
+
const resolvedSetData = options.setData ?? noop;
|
|
357
|
+
// The context's `setData` resolves with a commit; a mock usually returns
|
|
358
|
+
// nothing, so answer as an on-time write unless the mock says otherwise.
|
|
359
|
+
const contextSetData = async (...args) => (await Reflect.apply(resolvedSetData, undefined, args)) ?? { late: false };
|
|
360
|
+
const blobPrefix = options.blobPrefix ?? DEFAULT_BLOB_URL_PREFIX;
|
|
361
|
+
const scenarioAssetPrefix = options.scenarioAssetPrefix ?? DEFAULT_ASSET_URL_PREFIX;
|
|
362
|
+
const sharedAssetPrefix = options.sharedAssetPrefix ?? DEFAULT_SHARED_ASSET_URL_PREFIX;
|
|
363
|
+
const resolvedCallbacks = {
|
|
364
|
+
requestTick: asyncNoop,
|
|
365
|
+
renameOwnTeam: asyncNoop,
|
|
366
|
+
uploadBlob: asyncEmptyReference,
|
|
367
|
+
getAssetUrl: makeGetAssetUrl(blobPrefix, scenarioAssetPrefix, sharedAssetPrefix),
|
|
368
|
+
readAsset: asyncEmptyAsset,
|
|
369
|
+
getPastState: asyncEmpty,
|
|
370
|
+
getAllTeamsParticipants: async () => [],
|
|
371
|
+
setThemePreference: noop,
|
|
372
|
+
endpoints: {},
|
|
373
|
+
...options.callbacks,
|
|
374
|
+
};
|
|
375
|
+
const dataValue = {
|
|
376
|
+
data: (options.data ?? {}),
|
|
377
|
+
settings: options.settings ?? {},
|
|
378
|
+
completedTicks: options.completedTicks ?? 0,
|
|
379
|
+
isRoundProcessing: options.isRoundProcessing ?? false,
|
|
380
|
+
setData: contextSetData,
|
|
381
|
+
};
|
|
382
|
+
const participant = options.participant !== undefined
|
|
383
|
+
? options.participant
|
|
384
|
+
: options.viewerRole === 'observer'
|
|
385
|
+
? null
|
|
386
|
+
: DEFAULT_PARTICIPANT;
|
|
387
|
+
const providers = [
|
|
388
|
+
provide(ViewerStorageContext, {
|
|
389
|
+
sessionKey: options.sessionKey ?? DEFAULT_SESSION_KEY,
|
|
390
|
+
surface: 'player',
|
|
391
|
+
viewer: participant?.id ?? options.viewerRole ?? 'participant',
|
|
392
|
+
}),
|
|
393
|
+
provide(AssetContext, {
|
|
394
|
+
getAssetUrl: resolvedCallbacks.getAssetUrl,
|
|
395
|
+
locale: (options.locale ?? DEFAULT_LOCALE).locale,
|
|
396
|
+
}),
|
|
397
|
+
provide(PastStateContext, resolvedCallbacks.getPastState),
|
|
398
|
+
provide(UserInterfaceThemeContext, options.theme ?? createMockTheme()),
|
|
399
|
+
provide(UserInterfaceThemePreferenceContext, options.themePreference ?? 'system'),
|
|
400
|
+
provide(UserInterfaceLocaleContext, options.locale ?? DEFAULT_LOCALE),
|
|
401
|
+
provide(UserInterfaceViewerRoleContext, options.viewerRole ?? 'participant'),
|
|
402
|
+
provide(UserInterfaceRunKindContext, options.runKind ?? 'session'),
|
|
403
|
+
// An observer has no caller identity, so the participant defaults to
|
|
404
|
+
// null in that role unless the caller overrides it explicitly.
|
|
405
|
+
provide(UserInterfaceParticipantContext, participant),
|
|
406
|
+
provide(UserInterfaceParticipantsContext, options.participants ?? []),
|
|
407
|
+
provide(UserInterfaceTeamsContext, options.teams ?? []),
|
|
408
|
+
provide(UserInterfaceOwnTeamContext, resolveOwnTeam(options)),
|
|
409
|
+
provide(UserInterfaceOwnTeamJoinLinkContext, options.ownTeamJoinLink ?? null),
|
|
410
|
+
provide(UserInterfaceTimerContext, options.timer ?? DEFAULT_TIMER),
|
|
411
|
+
provide(UserInterfaceValidationContext, {
|
|
412
|
+
rejections: options.validationRejections ?? [],
|
|
413
|
+
clearRejections: options.clearRejections ?? noop,
|
|
414
|
+
}),
|
|
415
|
+
provide(UserInterfaceDataContext, dataValue),
|
|
416
|
+
provide(UserInterfaceCallbacksContext, resolvedCallbacks),
|
|
417
|
+
];
|
|
418
|
+
const result = render(wrapProviders(providers, ui));
|
|
419
|
+
return { ...result, setData: resolvedSetData, callbacks: resolvedCallbacks };
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Renders a component wrapped in every settings editor context provider: the
|
|
423
|
+
* `setup` slot and the `scenario_editor` slot share them.
|
|
424
|
+
*
|
|
425
|
+
* `updateSettings` defaults to a no-op rather than a spy, so pass your own
|
|
426
|
+
* `vi.fn()` to assert on it.
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* ```tsx
|
|
430
|
+
* const updateSettings = vi.fn();
|
|
431
|
+
* renderSettingsEditor(<App />, {
|
|
432
|
+
* settings: { engine: { MaxPlayers: 10 } },
|
|
433
|
+
* updateSettings,
|
|
434
|
+
* });
|
|
435
|
+
*
|
|
436
|
+
* fireEvent.change(screen.getByLabelText('Max Players'), { target: { value: '20' } });
|
|
437
|
+
* expect(updateSettings).toHaveBeenCalled();
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
export function renderSettingsEditor(ui, options = {}) {
|
|
441
|
+
const resolvedUpdateSettings = (options.updateSettings ?? asyncNoop);
|
|
442
|
+
const blobPrefix = options.blobPrefix ?? DEFAULT_BLOB_URL_PREFIX;
|
|
443
|
+
const scenarioAssetPrefix = options.scenarioAssetPrefix ?? DEFAULT_ASSET_URL_PREFIX;
|
|
444
|
+
const sharedAssetPrefix = options.sharedAssetPrefix ?? DEFAULT_SHARED_ASSET_URL_PREFIX;
|
|
445
|
+
const resolvedCallbacks = {
|
|
446
|
+
uploadBlob: asyncEmptyReference,
|
|
447
|
+
getAssetUrl: makeGetAssetUrl(blobPrefix, scenarioAssetPrefix, sharedAssetPrefix),
|
|
448
|
+
readAsset: asyncEmptyAsset,
|
|
449
|
+
setThemePreference: noop,
|
|
450
|
+
requestPlay: noop,
|
|
451
|
+
endpoints: {},
|
|
452
|
+
...options.callbacks,
|
|
453
|
+
};
|
|
454
|
+
const settings = options.settings ?? {};
|
|
455
|
+
const editorValue = {
|
|
456
|
+
settings,
|
|
457
|
+
updateSettings: resolvedUpdateSettings,
|
|
458
|
+
configures: options.configures ?? Object.keys(settings),
|
|
459
|
+
};
|
|
460
|
+
const providers = [
|
|
461
|
+
provide(ViewerStorageContext, {
|
|
462
|
+
sessionKey: options.sessionKey ?? DEFAULT_SESSION_KEY,
|
|
463
|
+
surface: 'settings-editor',
|
|
464
|
+
viewer: 'facilitator',
|
|
465
|
+
}),
|
|
466
|
+
provide(AssetContext, {
|
|
467
|
+
getAssetUrl: resolvedCallbacks.getAssetUrl,
|
|
468
|
+
locale: (options.locale ?? DEFAULT_LOCALE).locale,
|
|
469
|
+
}),
|
|
470
|
+
provide(SettingsEditorThemeContext, options.theme ?? createMockSettingsEditorTheme()),
|
|
471
|
+
provide(SettingsEditorThemePreferenceContext, options.themePreference ?? 'system'),
|
|
472
|
+
provide(SettingsEditorLocaleContext, options.locale ?? DEFAULT_LOCALE),
|
|
473
|
+
provide(SettingsEditorContext, editorValue),
|
|
474
|
+
provide(SettingsEditorCallbacksContext, resolvedCallbacks),
|
|
475
|
+
];
|
|
476
|
+
const result = render(wrapProviders(providers, ui));
|
|
477
|
+
return { ...result, updateSettings: resolvedUpdateSettings, callbacks: resolvedCallbacks };
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Renders a component wrapped in all SessionUserInterface context providers.
|
|
481
|
+
*
|
|
482
|
+
* Callbacks you do not supply default to no-ops rather than spies, so pass your
|
|
483
|
+
* own `vi.fn()` for any you intend to assert on.
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```tsx
|
|
487
|
+
* const resetGame = vi.fn();
|
|
488
|
+
* renderSessionUserInterface(<SessionDashboard />, {
|
|
489
|
+
* sessionData: { teams: { 'team-1': createMockTeamState() } },
|
|
490
|
+
* teams: [{ teamId: 'team-1', index: 0, name: 'Team 1' }],
|
|
491
|
+
* callbacks: { resetGame },
|
|
492
|
+
* });
|
|
493
|
+
*
|
|
494
|
+
* fireEvent.click(screen.getByText('Reset All'));
|
|
495
|
+
* expect(resetGame).toHaveBeenCalled();
|
|
496
|
+
* ```
|
|
497
|
+
*/
|
|
498
|
+
export function renderSessionUserInterface(ui, options = {}) {
|
|
499
|
+
const resolvedSetData = options.setData ?? noop;
|
|
500
|
+
// The shared-scope form calls the mock with the update alone, as the host's
|
|
501
|
+
// own `setData` is called.
|
|
502
|
+
const contextSetData = async (...args) => {
|
|
503
|
+
Reflect.apply(resolvedSetData, undefined, args);
|
|
504
|
+
};
|
|
505
|
+
const blobPrefix = options.blobPrefix ?? DEFAULT_BLOB_URL_PREFIX;
|
|
506
|
+
const scenarioAssetPrefix = options.scenarioAssetPrefix ?? DEFAULT_ASSET_URL_PREFIX;
|
|
507
|
+
const sharedAssetPrefix = options.sharedAssetPrefix ?? DEFAULT_SHARED_ASSET_URL_PREFIX;
|
|
508
|
+
const resolvedCallbacks = {
|
|
509
|
+
uploadBlob: asyncEmptyReference,
|
|
510
|
+
getAssetUrl: makeGetAssetUrl(blobPrefix, scenarioAssetPrefix, sharedAssetPrefix),
|
|
511
|
+
readAsset: asyncEmptyAsset,
|
|
512
|
+
getPastState: asyncEmpty,
|
|
513
|
+
requestTick: asyncNoop,
|
|
514
|
+
pauseTimer: asyncNoop,
|
|
515
|
+
resumeTimer: asyncNoop,
|
|
516
|
+
resetGame: asyncNoop,
|
|
517
|
+
undoTick: asyncNoop,
|
|
518
|
+
extendTick: asyncNoop,
|
|
519
|
+
openHostOverlay: asyncNoop,
|
|
520
|
+
setWaitingRoom: asyncNoop,
|
|
521
|
+
resetWalkthrough: asyncNoop,
|
|
522
|
+
createTeam: asyncNoop,
|
|
523
|
+
renameTeam: asyncNoop,
|
|
524
|
+
renameParticipant: asyncNoop,
|
|
525
|
+
moveParticipant: asyncNoop,
|
|
526
|
+
deleteTeam: asyncNoop,
|
|
527
|
+
setThemePreference: noop,
|
|
528
|
+
endpoints: {},
|
|
529
|
+
...options.callbacks,
|
|
530
|
+
};
|
|
531
|
+
const dataValue = {
|
|
532
|
+
// The provider holds the untyped shape; the surface's own
|
|
533
|
+
// `useSessionUserInterfaceData<SessionData>` types it back.
|
|
534
|
+
sessionData: {
|
|
535
|
+
shared: (options.sessionData?.shared ?? createMockSharedState()),
|
|
536
|
+
teams: (options.sessionData?.teams ?? {}),
|
|
537
|
+
},
|
|
538
|
+
settings: options.settings ?? {},
|
|
539
|
+
setData: contextSetData,
|
|
540
|
+
};
|
|
541
|
+
const providers = [
|
|
542
|
+
provide(ViewerStorageContext, {
|
|
543
|
+
sessionKey: options.sessionKey ?? DEFAULT_SESSION_KEY,
|
|
544
|
+
surface: 'session',
|
|
545
|
+
viewer: 'facilitator',
|
|
546
|
+
}),
|
|
547
|
+
provide(AssetContext, {
|
|
548
|
+
getAssetUrl: resolvedCallbacks.getAssetUrl,
|
|
549
|
+
locale: (options.locale ?? DEFAULT_LOCALE).locale,
|
|
550
|
+
}),
|
|
551
|
+
provide(PastStateContext, resolvedCallbacks.getPastState),
|
|
552
|
+
provide(SessionUserInterfaceThemeContext, options.theme ?? createMockSessionTheme()),
|
|
553
|
+
provide(SessionUserInterfaceThemePreferenceContext, options.themePreference ?? 'system'),
|
|
554
|
+
provide(SessionUserInterfaceLocaleContext, options.locale ?? DEFAULT_LOCALE),
|
|
555
|
+
provide(SessionUserInterfaceParticipantsContext, options.participants ?? []),
|
|
556
|
+
provide(SessionUserInterfaceTeamsContext, options.teams ?? []),
|
|
557
|
+
provide(SessionUserInterfaceTeamConfigurationContext, options.teamConfiguration ?? DEFAULT_TEAM_CONFIGURATION),
|
|
558
|
+
provide(SessionUserInterfaceMetaContext, options.sessionMeta ?? DEFAULT_SESSION_META),
|
|
559
|
+
provide(SessionUserInterfaceTeamLinksContext, options.teamLinks ?? []),
|
|
560
|
+
provide(UserInterfaceRunKindContext, options.runKind ?? 'session'),
|
|
561
|
+
provide(SessionUserInterfaceHostOverlaysContext, options.hostOverlays ?? []),
|
|
562
|
+
provide(SessionUserInterfaceWalkthroughLinkContext, options.walkthroughLink ?? null),
|
|
563
|
+
provide(SessionUserInterfaceHookFailuresContext, {
|
|
564
|
+
failures: options.hookFailures ?? [],
|
|
565
|
+
clearFailures: options.clearHookFailures ?? noop,
|
|
566
|
+
}),
|
|
567
|
+
provide(SessionUserInterfaceDataContext, dataValue),
|
|
568
|
+
provide(SessionUserInterfaceCallbacksContext, resolvedCallbacks),
|
|
569
|
+
];
|
|
570
|
+
const result = render(wrapProviders(providers, ui));
|
|
571
|
+
return { ...result, setData: resolvedSetData, callbacks: resolvedCallbacks };
|
|
572
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error a `getPastState` call rejects with on a genuine read fault.
|
|
3
|
+
*
|
|
4
|
+
* `getPastState` (§12) answers with the rows its query reaches, so every
|
|
5
|
+
* *expected* miss — a tick at or above the head, an undone or reset tick, an
|
|
6
|
+
* out-of-range tick, an unknown team, a capability the surface may not read —
|
|
7
|
+
* is simply an absent row. It rejects with a `TickSnapshotError` only when the
|
|
8
|
+
* read itself fails: the host is not connected, the connection resets
|
|
9
|
+
* mid-flight, or the host reports an unexpected fault. This is deliberately
|
|
10
|
+
* distinct from `MutationError`, which is for fallible *writes*; a read never
|
|
11
|
+
* conflicts, is never rejected by a validator, and is never denied.
|
|
12
|
+
*
|
|
13
|
+
* ```typescript
|
|
14
|
+
* try {
|
|
15
|
+
* const [past] = await callbacks.getPastState({ fromTick: day });
|
|
16
|
+
* if (!past) {
|
|
17
|
+
* // expected: that day is not reviewable (out of range, undone, reset)
|
|
18
|
+
* }
|
|
19
|
+
* } catch (e) {
|
|
20
|
+
* if (e instanceof TickSnapshotError) {
|
|
21
|
+
* // transport/host fault — surface a retry affordance
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare class TickSnapshotError extends Error {
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error a `getPastState` call rejects with on a genuine read fault.
|
|
3
|
+
*
|
|
4
|
+
* `getPastState` (§12) answers with the rows its query reaches, so every
|
|
5
|
+
* *expected* miss — a tick at or above the head, an undone or reset tick, an
|
|
6
|
+
* out-of-range tick, an unknown team, a capability the surface may not read —
|
|
7
|
+
* is simply an absent row. It rejects with a `TickSnapshotError` only when the
|
|
8
|
+
* read itself fails: the host is not connected, the connection resets
|
|
9
|
+
* mid-flight, or the host reports an unexpected fault. This is deliberately
|
|
10
|
+
* distinct from `MutationError`, which is for fallible *writes*; a read never
|
|
11
|
+
* conflicts, is never rejected by a validator, and is never denied.
|
|
12
|
+
*
|
|
13
|
+
* ```typescript
|
|
14
|
+
* try {
|
|
15
|
+
* const [past] = await callbacks.getPastState({ fromTick: day });
|
|
16
|
+
* if (!past) {
|
|
17
|
+
* // expected: that day is not reviewable (out of range, undone, reset)
|
|
18
|
+
* }
|
|
19
|
+
* } catch (e) {
|
|
20
|
+
* if (e instanceof TickSnapshotError) {
|
|
21
|
+
* // transport/host fault — surface a retry affordance
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export class TickSnapshotError extends Error {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'TickSnapshotError';
|
|
30
|
+
// Restore the prototype chain so `instanceof` works after transpilation
|
|
31
|
+
// to ES5-targeted output.
|
|
32
|
+
Object.setPrototypeOf(this, TickSnapshotError.prototype);
|
|
33
|
+
}
|
|
34
|
+
}
|