@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
package/lib/e2e.d.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end testing helpers for Serious Gaming SDK consumers.
|
|
3
|
+
*
|
|
4
|
+
* Selectors and API helpers for writing Playwright tests against a game running
|
|
5
|
+
* on the dev server. No dependency on Playwright itself — pass the selectors to
|
|
6
|
+
* your own `page.locator()` / `page.frameLocator()` calls.
|
|
7
|
+
*
|
|
8
|
+
* What you drive is your own game's surface inside the iframe; the console
|
|
9
|
+
* around it is a handful of URLs and one drawer, which is why this module is
|
|
10
|
+
* mostly URLs. Three things are worth knowing before writing a test:
|
|
11
|
+
*
|
|
12
|
+
* - Every surface is directly addressable. `/team/2/player/1` is the first
|
|
13
|
+
* participant of the second team, with no lookup and no redirect.
|
|
14
|
+
* - `?bare` removes the dev chrome entirely, so a change to the console cannot
|
|
15
|
+
* fail a test about your game. {@link DEV_SERVER.bare} adds it to any URL.
|
|
16
|
+
* - The helpers address the server `SERIOUSGAMING_DEV_URL` names, which is
|
|
17
|
+
* what a Playwright `globalSetup` running `startDevServer` sets, so a test
|
|
18
|
+
* on free ports passes no options. `{ url }` or `{ port }` picks another.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { DEV_SERVER, runCommand, getState } from '@seriousgaming/sdk/e2e';
|
|
23
|
+
*
|
|
24
|
+
* test('the dashboard shows the tick', async ({ page }) => {
|
|
25
|
+
* await runCommand({ type: 'reset' });
|
|
26
|
+
* await page.goto(DEV_SERVER.bare(DEV_SERVER.playerUrl(1)));
|
|
27
|
+
*
|
|
28
|
+
* const frame = page.frameLocator(DEV_SERVER.iframeSelector('ui'));
|
|
29
|
+
* await expect(frame.locator('#tick')).toHaveText('0');
|
|
30
|
+
*
|
|
31
|
+
* await runCommand({ type: 'tick' });
|
|
32
|
+
* await expect(frame.locator('#tick')).toHaveText('1');
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
import type { RunKind } from './build/signatures.js';
|
|
37
|
+
import type { Command, MutationCommand, QueryCommand, QueryResult } from './commands.js';
|
|
38
|
+
import type { MutationResult, StateProjection } from './projection.js';
|
|
39
|
+
export type { Command, MutationCommand, MutationResult, QueryCommand, QueryResult, StateProjection, };
|
|
40
|
+
/**
|
|
41
|
+
* Default port the SDK dev server runs on.
|
|
42
|
+
*
|
|
43
|
+
* @internal Read {@link DEV_SERVER}`.DEFAULT_PORT` instead.
|
|
44
|
+
*/
|
|
45
|
+
export declare const DEV_SERVER_DEFAULT_PORT = 3000;
|
|
46
|
+
/**
|
|
47
|
+
* Selectors and URLs for the dev console.
|
|
48
|
+
*
|
|
49
|
+
* `selectors` values are CSS attribute selectors over `data-testid`. The set is
|
|
50
|
+
* deliberately small: the console is chrome around your game, and a test that
|
|
51
|
+
* drove the chrome would be a test of the SDK rather than of the game.
|
|
52
|
+
*/
|
|
53
|
+
export declare const DEV_SERVER: {
|
|
54
|
+
/** Default port the dev server runs on. */
|
|
55
|
+
readonly DEFAULT_PORT: 3000;
|
|
56
|
+
readonly selectors: {
|
|
57
|
+
/**
|
|
58
|
+
* The drawer handle. Bottom-right by default, and movable to any
|
|
59
|
+
* corner, so locate it by this selector rather than by position. Its
|
|
60
|
+
* presence is the signal that the console has mounted and connected —
|
|
61
|
+
* wait for it before pressing a key the console listens for.
|
|
62
|
+
*/
|
|
63
|
+
readonly devDrawerHandle: "[data-testid=\"dev-drawer-handle\"]";
|
|
64
|
+
/** The dev drawer itself, opened with the backtick key. */
|
|
65
|
+
readonly devDrawer: "[data-testid=\"dev-drawer\"]";
|
|
66
|
+
/** The drawer's locale control. */
|
|
67
|
+
readonly localeSelect: "[data-testid=\"locale\"]";
|
|
68
|
+
/**
|
|
69
|
+
* The host's Play control, which commits the configuration step and
|
|
70
|
+
* starts the session. Present only while the step is outstanding, and
|
|
71
|
+
* only for a game with a `setup` slot.
|
|
72
|
+
*/
|
|
73
|
+
readonly play: "[data-testid=\"play\"]";
|
|
74
|
+
/** The rendered chapter on the guide page. */
|
|
75
|
+
readonly guideContent: "[data-testid=\"guide-content\"]";
|
|
76
|
+
/** The team sheets on the print page. */
|
|
77
|
+
readonly printSheets: "[data-testid=\"print-sheets\"]";
|
|
78
|
+
};
|
|
79
|
+
/** The console: the game's own session surface, with dev chrome around it. */
|
|
80
|
+
readonly consoleUrl: () => string;
|
|
81
|
+
/**
|
|
82
|
+
* One participant's surface. `team` and `participant` are both 1-based,
|
|
83
|
+
* exactly as they read in the URL.
|
|
84
|
+
*/
|
|
85
|
+
readonly playerUrl: (team: number, participant?: number) => string;
|
|
86
|
+
/** A team's surface with no participant identity and no presence (§13.3). */
|
|
87
|
+
readonly observeUrl: (team: number) => string;
|
|
88
|
+
/** A team's waiting-room surface (§6.14). */
|
|
89
|
+
readonly waitingRoomUrl: (team: number) => string;
|
|
90
|
+
/**
|
|
91
|
+
* What a facilitator hands out: it asks which participant you are, or opens
|
|
92
|
+
* straight into their surface when the team holds one.
|
|
93
|
+
*/
|
|
94
|
+
readonly joinUrl: (team: number) => string;
|
|
95
|
+
/** The setup surface, before the session starts (§8.3). */
|
|
96
|
+
readonly setupUrl: () => string;
|
|
97
|
+
/**
|
|
98
|
+
* The scenario editor, on one scenario (§6.7.1).
|
|
99
|
+
*
|
|
100
|
+
* It is always opened on a scenario: an editor with nowhere to save is one
|
|
101
|
+
* that wastes the work done in it. In the console this is reached from the
|
|
102
|
+
* dev drawer's Scenario tab rather than typed.
|
|
103
|
+
*/
|
|
104
|
+
readonly scenarioEditorUrl: (scenario: string) => string;
|
|
105
|
+
/** The facilitator guide (§6.9). */
|
|
106
|
+
readonly guideUrl: () => string;
|
|
107
|
+
/** One printable sheet per team. */
|
|
108
|
+
readonly printUrl: () => string;
|
|
109
|
+
/**
|
|
110
|
+
* The same URL with the dev chrome removed: exactly what a participant
|
|
111
|
+
* sees, which is what a test about your game should drive.
|
|
112
|
+
*/
|
|
113
|
+
readonly bare: (url: string) => string;
|
|
114
|
+
/**
|
|
115
|
+
* The `frameLocator` selector for a surface's iframe, by its identifier.
|
|
116
|
+
* A game holds one of each kind, so the identifier is the slot: `ui`,
|
|
117
|
+
* `session`, `waiting_room`, `setup` or `scenario_editor`.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const frame = page.frameLocator(DEV_SERVER.iframeSelector('ui'));
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
readonly iframeSelector: (identifier: string) => string;
|
|
125
|
+
/** A full URL to a dev server endpoint. */
|
|
126
|
+
readonly apiUrl: (path: string, port?: number) => string;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Which dev server a helper talks to. With neither field set, the server
|
|
130
|
+
* `SERIOUSGAMING_DEV_URL` names (as `startDevServer` in a Playwright
|
|
131
|
+
* `globalSetup` sets it), or else `localhost` on {@link DEV_SERVER_DEFAULT_PORT}.
|
|
132
|
+
*/
|
|
133
|
+
export interface DevServerOptions {
|
|
134
|
+
/** The server's address, such as `startDevServer()`'s `url`. Wins over `port`. */
|
|
135
|
+
url?: string;
|
|
136
|
+
/** Dev server port on `localhost`. */
|
|
137
|
+
port?: number;
|
|
138
|
+
/**
|
|
139
|
+
* Which run to address: the session (the default), or the walkthrough
|
|
140
|
+
* beside it (§5.7). The first command sent to the walkthrough builds it.
|
|
141
|
+
*/
|
|
142
|
+
run?: RunKind;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Run one command against the running session.
|
|
146
|
+
*
|
|
147
|
+
* The same closed command set the console and the CLI use, so anything you can
|
|
148
|
+
* do by hand you can do from a test: `{ type: 'tick' }`, `{ type: 'reset' }`,
|
|
149
|
+
* `{ type: 'updateContext', payload: { … } }`, `{ type: 'addTeam' }`. It is
|
|
150
|
+
* typed against that set, so a misspelled command or field fails to compile
|
|
151
|
+
* rather than coming back as a 400.
|
|
152
|
+
*
|
|
153
|
+
* A mutation settles as `{ ok: true }` or `{ ok: false, outcome }`: a refusal
|
|
154
|
+
* is an answer rather than a throw, since a tick past the limit is an answer,
|
|
155
|
+
* not a fault. A read (`getState`, `callEndpoint`, `getPastState`,
|
|
156
|
+
* `getAllTeamsParticipants`) answers with that read's own result; use
|
|
157
|
+
* {@link getState} for the common one. Only a transport failure throws, and
|
|
158
|
+
* so does a command the server does not recognise.
|
|
159
|
+
*/
|
|
160
|
+
export declare function runCommand(command: MutationCommand, options?: DevServerOptions): Promise<MutationResult>;
|
|
161
|
+
export declare function runCommand(command: QueryCommand, options?: DevServerOptions): Promise<QueryResult>;
|
|
162
|
+
/**
|
|
163
|
+
* The whole session, as one projection: teams, contexts, participants, the
|
|
164
|
+
* resolved configuration, the timer, and anything the runtime has gone wrong
|
|
165
|
+
* about. Assert on state without going through a surface.
|
|
166
|
+
*
|
|
167
|
+
* @throws if the server responds with a non-OK status.
|
|
168
|
+
*/
|
|
169
|
+
export declare function getState(options?: DevServerOptions): Promise<StateProjection>;
|
|
170
|
+
/**
|
|
171
|
+
* Return the session to tick 0, so one test does not inherit another's state.
|
|
172
|
+
* Call it in `test.beforeEach`.
|
|
173
|
+
*
|
|
174
|
+
* **It leaves the session started.** The tick counter goes back to zero and
|
|
175
|
+
* the contexts with it; the waiting room is not reopened, and a game that owes
|
|
176
|
+
* a configuration step is not returned to it. Only `{ type: 'reconfigure' }`
|
|
177
|
+
* goes back to the configuration step, and a game with a `setup` slot needs
|
|
178
|
+
* an explicit `{ type: 'play' }` before any player URL has a game behind it. A test that assumed otherwise spends its time looking at an empty
|
|
179
|
+
* player URL.
|
|
180
|
+
*
|
|
181
|
+
* @throws if the server responds with a non-OK status.
|
|
182
|
+
*/
|
|
183
|
+
export declare function resetSession(options?: DevServerOptions): Promise<void>;
|
|
184
|
+
export interface ReinitialiseOptions extends DevServerOptions {
|
|
185
|
+
/** Teams to start with. Omit to keep the count the session has. */
|
|
186
|
+
teams?: number;
|
|
187
|
+
/** Participants per team: one count, or one per team. Zero is allowed. */
|
|
188
|
+
participants?: number | number[];
|
|
189
|
+
/**
|
|
190
|
+
* A scenario to start on. Omit for the game's default, as a fresh
|
|
191
|
+
* `sdk dev` would; pass `null` for the settings defaults.
|
|
192
|
+
*/
|
|
193
|
+
scenarioId?: string | null;
|
|
194
|
+
locale?: string;
|
|
195
|
+
/**
|
|
196
|
+
* Press Play on a game with a configuration step (§8.3). Default `true`,
|
|
197
|
+
* so every player URL has a game behind it; `false` to test the step.
|
|
198
|
+
*/
|
|
199
|
+
play?: boolean;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Start the session again from the package, as a test's known starting point.
|
|
203
|
+
*
|
|
204
|
+
* Wider than {@link resetSession}, which returns to tick 0 but keeps the team
|
|
205
|
+
* count, roster, scenario and locale an earlier test left behind. This
|
|
206
|
+
* re-resolves them, presses Play where the game has a configuration step, and
|
|
207
|
+
* disarms any fault an earlier test armed. The waiting room is left as it is;
|
|
208
|
+
* open or close it with `{ type: 'setWaitingRoom', open }`.
|
|
209
|
+
*
|
|
210
|
+
* @throws if the session refuses, for instance a team count outside the game's
|
|
211
|
+
* bounds.
|
|
212
|
+
*/
|
|
213
|
+
export declare function reinitialiseSession(options?: ReinitialiseOptions): Promise<void>;
|
|
214
|
+
export interface WaitOptions extends DevServerOptions {
|
|
215
|
+
/** Give up after this long. Default 10 seconds. */
|
|
216
|
+
timeout?: number;
|
|
217
|
+
/** How often to look. Default 100 milliseconds. */
|
|
218
|
+
interval?: number;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Wait until the session satisfies `predicate`, and return the state that did.
|
|
222
|
+
*
|
|
223
|
+
* For something that happens after the command that caused it has answered:
|
|
224
|
+
* an automatic tick, a hook's write, a participant connecting when a page
|
|
225
|
+
* opens. Poll the session rather than sleeping for a guess at how long it
|
|
226
|
+
* takes.
|
|
227
|
+
*
|
|
228
|
+
* @throws when `timeout` passes first, with the last state it saw.
|
|
229
|
+
*/
|
|
230
|
+
export declare function waitForState(predicate: (state: StateProjection) => boolean, options?: WaitOptions): Promise<StateProjection>;
|
|
231
|
+
/**
|
|
232
|
+
* Move the clock of a dev server started with `--fake-clock` forward, running
|
|
233
|
+
* every automatic tick that falls due on the way (§11). Resolves once they
|
|
234
|
+
* have run.
|
|
235
|
+
*
|
|
236
|
+
* @throws when the server runs on the real clock.
|
|
237
|
+
*/
|
|
238
|
+
export declare function advanceClock(ms: number, options?: DevServerOptions): Promise<void>;
|
package/lib/e2e.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end testing helpers for Serious Gaming SDK consumers.
|
|
3
|
+
*
|
|
4
|
+
* Selectors and API helpers for writing Playwright tests against a game running
|
|
5
|
+
* on the dev server. No dependency on Playwright itself — pass the selectors to
|
|
6
|
+
* your own `page.locator()` / `page.frameLocator()` calls.
|
|
7
|
+
*
|
|
8
|
+
* What you drive is your own game's surface inside the iframe; the console
|
|
9
|
+
* around it is a handful of URLs and one drawer, which is why this module is
|
|
10
|
+
* mostly URLs. Three things are worth knowing before writing a test:
|
|
11
|
+
*
|
|
12
|
+
* - Every surface is directly addressable. `/team/2/player/1` is the first
|
|
13
|
+
* participant of the second team, with no lookup and no redirect.
|
|
14
|
+
* - `?bare` removes the dev chrome entirely, so a change to the console cannot
|
|
15
|
+
* fail a test about your game. {@link DEV_SERVER.bare} adds it to any URL.
|
|
16
|
+
* - The helpers address the server `SERIOUSGAMING_DEV_URL` names, which is
|
|
17
|
+
* what a Playwright `globalSetup` running `startDevServer` sets, so a test
|
|
18
|
+
* on free ports passes no options. `{ url }` or `{ port }` picks another.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { DEV_SERVER, runCommand, getState } from '@seriousgaming/sdk/e2e';
|
|
23
|
+
*
|
|
24
|
+
* test('the dashboard shows the tick', async ({ page }) => {
|
|
25
|
+
* await runCommand({ type: 'reset' });
|
|
26
|
+
* await page.goto(DEV_SERVER.bare(DEV_SERVER.playerUrl(1)));
|
|
27
|
+
*
|
|
28
|
+
* const frame = page.frameLocator(DEV_SERVER.iframeSelector('ui'));
|
|
29
|
+
* await expect(frame.locator('#tick')).toHaveText('0');
|
|
30
|
+
*
|
|
31
|
+
* await runCommand({ type: 'tick' });
|
|
32
|
+
* await expect(frame.locator('#tick')).toHaveText('1');
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Default port the SDK dev server runs on.
|
|
38
|
+
*
|
|
39
|
+
* @internal Read {@link DEV_SERVER}`.DEFAULT_PORT` instead.
|
|
40
|
+
*/
|
|
41
|
+
export const DEV_SERVER_DEFAULT_PORT = 3000;
|
|
42
|
+
/**
|
|
43
|
+
* Selectors and URLs for the dev console.
|
|
44
|
+
*
|
|
45
|
+
* `selectors` values are CSS attribute selectors over `data-testid`. The set is
|
|
46
|
+
* deliberately small: the console is chrome around your game, and a test that
|
|
47
|
+
* drove the chrome would be a test of the SDK rather than of the game.
|
|
48
|
+
*/
|
|
49
|
+
export const DEV_SERVER = {
|
|
50
|
+
/** Default port the dev server runs on. */
|
|
51
|
+
DEFAULT_PORT: DEV_SERVER_DEFAULT_PORT,
|
|
52
|
+
selectors: {
|
|
53
|
+
/**
|
|
54
|
+
* The drawer handle. Bottom-right by default, and movable to any
|
|
55
|
+
* corner, so locate it by this selector rather than by position. Its
|
|
56
|
+
* presence is the signal that the console has mounted and connected —
|
|
57
|
+
* wait for it before pressing a key the console listens for.
|
|
58
|
+
*/
|
|
59
|
+
devDrawerHandle: '[data-testid="dev-drawer-handle"]',
|
|
60
|
+
/** The dev drawer itself, opened with the backtick key. */
|
|
61
|
+
devDrawer: '[data-testid="dev-drawer"]',
|
|
62
|
+
/** The drawer's locale control. */
|
|
63
|
+
localeSelect: '[data-testid="locale"]',
|
|
64
|
+
/**
|
|
65
|
+
* The host's Play control, which commits the configuration step and
|
|
66
|
+
* starts the session. Present only while the step is outstanding, and
|
|
67
|
+
* only for a game with a `setup` slot.
|
|
68
|
+
*/
|
|
69
|
+
play: '[data-testid="play"]',
|
|
70
|
+
/** The rendered chapter on the guide page. */
|
|
71
|
+
guideContent: '[data-testid="guide-content"]',
|
|
72
|
+
/** The team sheets on the print page. */
|
|
73
|
+
printSheets: '[data-testid="print-sheets"]',
|
|
74
|
+
},
|
|
75
|
+
/** The console: the game's own session surface, with dev chrome around it. */
|
|
76
|
+
consoleUrl: () => '/',
|
|
77
|
+
/**
|
|
78
|
+
* One participant's surface. `team` and `participant` are both 1-based,
|
|
79
|
+
* exactly as they read in the URL.
|
|
80
|
+
*/
|
|
81
|
+
playerUrl: (team, participant = 1) => `/team/${team}/player/${participant}`,
|
|
82
|
+
/** A team's surface with no participant identity and no presence (§13.3). */
|
|
83
|
+
observeUrl: (team) => `/team/${team}/observe`,
|
|
84
|
+
/** A team's waiting-room surface (§6.14). */
|
|
85
|
+
waitingRoomUrl: (team) => `/team/${team}/waiting-room`,
|
|
86
|
+
/**
|
|
87
|
+
* What a facilitator hands out: it asks which participant you are, or opens
|
|
88
|
+
* straight into their surface when the team holds one.
|
|
89
|
+
*/
|
|
90
|
+
joinUrl: (team) => `/team/${team}/join`,
|
|
91
|
+
/** The setup surface, before the session starts (§8.3). */
|
|
92
|
+
setupUrl: () => '/setup',
|
|
93
|
+
/**
|
|
94
|
+
* The scenario editor, on one scenario (§6.7.1).
|
|
95
|
+
*
|
|
96
|
+
* It is always opened on a scenario: an editor with nowhere to save is one
|
|
97
|
+
* that wastes the work done in it. In the console this is reached from the
|
|
98
|
+
* dev drawer's Scenario tab rather than typed.
|
|
99
|
+
*/
|
|
100
|
+
scenarioEditorUrl: (scenario) => `/scenario-editor?scenario=${encodeURIComponent(scenario)}`,
|
|
101
|
+
/** The facilitator guide (§6.9). */
|
|
102
|
+
guideUrl: () => '/guide',
|
|
103
|
+
/** One printable sheet per team. */
|
|
104
|
+
printUrl: () => '/print',
|
|
105
|
+
/**
|
|
106
|
+
* The same URL with the dev chrome removed: exactly what a participant
|
|
107
|
+
* sees, which is what a test about your game should drive.
|
|
108
|
+
*/
|
|
109
|
+
bare: (url) => (url.includes('?') ? `${url}&bare` : `${url}?bare`),
|
|
110
|
+
/**
|
|
111
|
+
* The `frameLocator` selector for a surface's iframe, by its identifier.
|
|
112
|
+
* A game holds one of each kind, so the identifier is the slot: `ui`,
|
|
113
|
+
* `session`, `waiting_room`, `setup` or `scenario_editor`.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* const frame = page.frameLocator(DEV_SERVER.iframeSelector('ui'));
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
iframeSelector: (identifier) => `iframe[title="${identifier}"]`,
|
|
121
|
+
/** A full URL to a dev server endpoint. */
|
|
122
|
+
apiUrl: (path, port = DEV_SERVER_DEFAULT_PORT) => `http://localhost:${port}${path}`,
|
|
123
|
+
};
|
|
124
|
+
/** The variable naming the dev server, read by these helpers and by the CLI. */
|
|
125
|
+
const DEV_URL_ENV_VAR = 'SERIOUSGAMING_DEV_URL';
|
|
126
|
+
function serverOrigin(options) {
|
|
127
|
+
if (options.url !== undefined)
|
|
128
|
+
return options.url.replace(/\/+$/, '');
|
|
129
|
+
if (options.port !== undefined)
|
|
130
|
+
return `http://localhost:${options.port}`;
|
|
131
|
+
// Read through `globalThis`: this module also loads where there is no `process`.
|
|
132
|
+
const environment = globalThis
|
|
133
|
+
.process?.env;
|
|
134
|
+
const named = environment?.[DEV_URL_ENV_VAR];
|
|
135
|
+
return named ? named.replace(/\/+$/, '') : `http://localhost:${DEV_SERVER_DEFAULT_PORT}`;
|
|
136
|
+
}
|
|
137
|
+
function commandUrl(path, options) {
|
|
138
|
+
const url = `${serverOrigin(options)}${path}`;
|
|
139
|
+
return options.run === 'walkthrough' ? `${url}?run=walkthrough` : url;
|
|
140
|
+
}
|
|
141
|
+
export async function runCommand(command, options = {}) {
|
|
142
|
+
const response = await fetch(commandUrl('/api/command', options), {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'Content-Type': 'application/json' },
|
|
145
|
+
body: JSON.stringify(command),
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
const body = (await response.json().catch(() => null));
|
|
149
|
+
throw new Error(`runCommand failed: HTTP ${response.status}${body?.error ? `: ${body.error}` : ''}`);
|
|
150
|
+
}
|
|
151
|
+
return (await response.json());
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The whole session, as one projection: teams, contexts, participants, the
|
|
155
|
+
* resolved configuration, the timer, and anything the runtime has gone wrong
|
|
156
|
+
* about. Assert on state without going through a surface.
|
|
157
|
+
*
|
|
158
|
+
* @throws if the server responds with a non-OK status.
|
|
159
|
+
*/
|
|
160
|
+
export async function getState(options = {}) {
|
|
161
|
+
const response = await fetch(commandUrl('/api/state', options));
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(`getState failed: HTTP ${response.status}`);
|
|
164
|
+
}
|
|
165
|
+
return (await response.json());
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Return the session to tick 0, so one test does not inherit another's state.
|
|
169
|
+
* Call it in `test.beforeEach`.
|
|
170
|
+
*
|
|
171
|
+
* **It leaves the session started.** The tick counter goes back to zero and
|
|
172
|
+
* the contexts with it; the waiting room is not reopened, and a game that owes
|
|
173
|
+
* a configuration step is not returned to it. Only `{ type: 'reconfigure' }`
|
|
174
|
+
* goes back to the configuration step, and a game with a `setup` slot needs
|
|
175
|
+
* an explicit `{ type: 'play' }` before any player URL has a game behind it. A test that assumed otherwise spends its time looking at an empty
|
|
176
|
+
* player URL.
|
|
177
|
+
*
|
|
178
|
+
* @throws if the server responds with a non-OK status.
|
|
179
|
+
*/
|
|
180
|
+
export async function resetSession(options = {}) {
|
|
181
|
+
await runCommand({ type: 'reset' }, options);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Start the session again from the package, as a test's known starting point.
|
|
185
|
+
*
|
|
186
|
+
* Wider than {@link resetSession}, which returns to tick 0 but keeps the team
|
|
187
|
+
* count, roster, scenario and locale an earlier test left behind. This
|
|
188
|
+
* re-resolves them, presses Play where the game has a configuration step, and
|
|
189
|
+
* disarms any fault an earlier test armed. The waiting room is left as it is;
|
|
190
|
+
* open or close it with `{ type: 'setWaitingRoom', open }`.
|
|
191
|
+
*
|
|
192
|
+
* @throws if the session refuses, for instance a team count outside the game's
|
|
193
|
+
* bounds.
|
|
194
|
+
*/
|
|
195
|
+
export async function reinitialiseSession(options = {}) {
|
|
196
|
+
const { teams, participants, scenarioId, locale, play = true, ...server } = options;
|
|
197
|
+
const result = await runCommand({
|
|
198
|
+
type: 'reinitialise',
|
|
199
|
+
...(teams === undefined ? {} : { teams }),
|
|
200
|
+
...(participants === undefined ? {} : { participants }),
|
|
201
|
+
...(scenarioId === undefined ? {} : { scenarioId }),
|
|
202
|
+
...(locale === undefined ? {} : { locale }),
|
|
203
|
+
}, server);
|
|
204
|
+
if (!result.ok)
|
|
205
|
+
throw new Error(`reinitialiseSession refused: ${result.outcome.message ?? result.outcome.kind}`);
|
|
206
|
+
const state = await getState(server);
|
|
207
|
+
for (const armed of state.faults) {
|
|
208
|
+
await runCommand({ type: 'disarmFault', fault: armed.kind }, server);
|
|
209
|
+
}
|
|
210
|
+
if (play && state.session.inSetup) {
|
|
211
|
+
await runCommand({ type: 'play' }, server);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Wait until the session satisfies `predicate`, and return the state that did.
|
|
216
|
+
*
|
|
217
|
+
* For something that happens after the command that caused it has answered:
|
|
218
|
+
* an automatic tick, a hook's write, a participant connecting when a page
|
|
219
|
+
* opens. Poll the session rather than sleeping for a guess at how long it
|
|
220
|
+
* takes.
|
|
221
|
+
*
|
|
222
|
+
* @throws when `timeout` passes first, with the last state it saw.
|
|
223
|
+
*/
|
|
224
|
+
export async function waitForState(predicate, options = {}) {
|
|
225
|
+
const { timeout = 10_000, interval = 100, ...server } = options;
|
|
226
|
+
const deadline = Date.now() + timeout;
|
|
227
|
+
for (;;) {
|
|
228
|
+
const state = await getState(server);
|
|
229
|
+
if (predicate(state))
|
|
230
|
+
return state;
|
|
231
|
+
if (Date.now() >= deadline) {
|
|
232
|
+
throw new Error(`waitForState timed out after ${timeout}ms with ${state.session.completedTicks} ticks completed`);
|
|
233
|
+
}
|
|
234
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Move the clock of a dev server started with `--fake-clock` forward, running
|
|
239
|
+
* every automatic tick that falls due on the way (§11). Resolves once they
|
|
240
|
+
* have run.
|
|
241
|
+
*
|
|
242
|
+
* @throws when the server runs on the real clock.
|
|
243
|
+
*/
|
|
244
|
+
export async function advanceClock(ms, options = {}) {
|
|
245
|
+
const result = await runCommand({ type: 'advanceClock', byMs: ms }, options);
|
|
246
|
+
if (!result.ok)
|
|
247
|
+
throw new Error(`advanceClock refused: ${result.outcome.message ?? result.outcome.kind}`);
|
|
248
|
+
}
|