@seriousgaming/sdk-tools 0.0.0-stage → 1.0.0-alpha.1
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 +93 -0
- package/README.md +36 -2
- package/dist/bundling/logic-exports.d.ts +34 -0
- package/dist/bundling/node-builtins.d.ts +21 -0
- package/dist/bundling/transpiler.d.ts +7 -0
- package/dist/cli.js +27368 -0
- package/dist/client/dev-server.d.ts +59 -0
- package/dist/client/index.d.ts +28 -0
- package/dist/client/stub-services.d.ts +32 -0
- package/dist/client/test-session.d.ts +195 -0
- package/dist/client.js +16884 -0
- package/dist/core/asset-reference-translator.d.ts +46 -0
- package/dist/core/clock.d.ts +57 -0
- package/dist/core/faults.d.ts +65 -0
- package/dist/core/game-settings.d.ts +65 -0
- package/dist/core/history.d.ts +89 -0
- package/dist/core/listen-with-fallback.d.ts +30 -0
- package/dist/core/mutation-queue.d.ts +61 -0
- package/dist/core/package-scenario.d.ts +89 -0
- package/dist/core/participant-registry.d.ts +109 -0
- package/dist/core/pending-changes.d.ts +14 -0
- package/dist/core/protocol.d.ts +83 -0
- package/dist/core/scenario-drafts.d.ts +32 -0
- package/dist/core/session-endpoints.d.ts +108 -0
- package/dist/core/session-gates.d.ts +75 -0
- package/dist/core/session-host.d.ts +94 -0
- package/dist/core/session-refusals.d.ts +56 -0
- package/dist/core/session-rewind.d.ts +32 -0
- package/dist/core/session-roster.d.ts +128 -0
- package/dist/core/session-surfaces.d.ts +32 -0
- package/dist/core/session-ticks.d.ts +18 -0
- package/dist/core/session-timers.d.ts +84 -0
- package/dist/core/session-writes.d.ts +40 -0
- package/dist/core/session.d.ts +385 -0
- package/dist/core/settings-locks.d.ts +22 -0
- package/dist/core/team-names.d.ts +52 -0
- package/dist/core/team-state-store.d.ts +209 -0
- package/dist/core/tick-timer.d.ts +75 -0
- package/dist/dev/blob-links.d.ts +10 -0
- package/dist/dev/console/assets/index-DgFglsnA.js +204 -0
- package/dist/dev/console/index.html +12 -0
- package/dist/dev/console-path.d.ts +19 -0
- package/dist/dev/lockfile.d.ts +124 -0
- package/dist/dev/package-watcher.d.ts +21 -0
- package/dist/dev/routes.d.ts +67 -0
- package/dist/dev/shell.d.ts +260 -0
- package/dist/dev/surfaces.d.ts +41 -0
- package/dist/dev/tools-identity.d.ts +23 -0
- package/dist/dev/view/batch.d.ts +172 -0
- package/dist/dev/view/commands.d.ts +75 -0
- package/dist/dev/view/route.d.ts +58 -0
- package/dist/dev/view/view.d.ts +78 -0
- package/dist/dev/view/viewer.d.ts +32 -0
- package/dist/discovery/config.d.ts +9 -0
- package/dist/discovery/manifest-paths.d.ts +21 -0
- package/dist/discovery/removed-fields.d.ts +90 -0
- package/dist/docs/api/deck.md +386 -0
- package/dist/docs/api/index.md +67 -0
- package/dist/docs/api/sdk-commands.md +286 -0
- package/dist/docs/api/sdk-e2e.md +433 -0
- package/dist/docs/api/sdk-projection.md +207 -0
- package/dist/docs/api/sdk-testing.md +214 -0
- package/dist/docs/api/sdk-tools-client.md +560 -0
- package/dist/docs/api/sdk-ui-testing.md +231 -0
- package/dist/docs/api/sdk-ui.md +1269 -0
- package/dist/docs/api/sdk.md +2140 -0
- package/dist/docs/changelog-0.x.md +1156 -0
- package/dist/docs/changelog.md +236 -0
- package/dist/docs/cli-and-dev-server.md +217 -0
- package/dist/docs/cli-error-codes.md +154 -0
- package/dist/docs/cli-reference.md +2488 -0
- package/dist/docs/cli-simulation-testing.md +222 -0
- package/dist/docs/concepts/index.md +46 -0
- package/dist/docs/concepts/terminology.md +322 -0
- package/dist/docs/environment-variables.md +102 -0
- package/dist/docs/getting-started.md +145 -0
- package/dist/docs/guides/ai-facilitation.md +220 -0
- package/dist/docs/guides/capabilities.md +460 -0
- package/dist/docs/guides/endpoints.md +335 -0
- package/dist/docs/guides/engine-testing.md +477 -0
- package/dist/docs/guides/engine.md +527 -0
- package/dist/docs/guides/file-handling.md +405 -0
- package/dist/docs/guides/games.md +176 -0
- package/dist/docs/guides/global-settings.md +94 -0
- package/dist/docs/guides/guide.md +123 -0
- package/dist/docs/guides/hooks.md +183 -0
- package/dist/docs/guides/index.md +51 -0
- package/dist/docs/guides/internationalization.md +288 -0
- package/dist/docs/guides/llm-integration.md +635 -0
- package/dist/docs/guides/multi-team-games.md +586 -0
- package/dist/docs/guides/patterns.md +391 -0
- package/dist/docs/guides/presentation.md +196 -0
- package/dist/docs/guides/production-constraints.md +130 -0
- package/dist/docs/guides/scenario-editor.md +143 -0
- package/dist/docs/guides/scenarios.md +515 -0
- package/dist/docs/guides/session.md +972 -0
- package/dist/docs/guides/settings.md +458 -0
- package/dist/docs/guides/setup.md +193 -0
- package/dist/docs/guides/speech-generation.md +233 -0
- package/dist/docs/guides/starter-game.md +164 -0
- package/dist/docs/guides/testing.md +575 -0
- package/dist/docs/guides/tick-controller.md +569 -0
- package/dist/docs/guides/type-safety.md +516 -0
- package/dist/docs/guides/ui.md +1389 -0
- package/dist/docs/guides/upgrading.md +636 -0
- package/dist/docs/guides/validators.md +217 -0
- package/dist/docs/guides/waiting-room.md +244 -0
- package/dist/docs/index.md +41 -0
- package/dist/docs/recipes/act-on-the-session-from-an-endpoint.md +95 -0
- package/dist/docs/recipes/adapt-a-game-for-walkthroughs.md +133 -0
- package/dist/docs/recipes/adapt-ui-for-observers.md +74 -0
- package/dist/docs/recipes/call-endpoint-from-ui.md +109 -0
- package/dist/docs/recipes/check-a-package-from-a-script.md +117 -0
- package/dist/docs/recipes/create-capability.md +66 -0
- package/dist/docs/recipes/create-endpoint.md +127 -0
- package/dist/docs/recipes/create-engine.md +111 -0
- package/dist/docs/recipes/create-game.md +70 -0
- package/dist/docs/recipes/create-guide.md +75 -0
- package/dist/docs/recipes/create-hook.md +80 -0
- package/dist/docs/recipes/create-presentation.md +142 -0
- package/dist/docs/recipes/create-scenario-editor.md +102 -0
- package/dist/docs/recipes/create-scenario.md +98 -0
- package/dist/docs/recipes/create-session-ui.md +116 -0
- package/dist/docs/recipes/create-settings.md +88 -0
- package/dist/docs/recipes/create-setup.md +115 -0
- package/dist/docs/recipes/create-tick-controller.md +65 -0
- package/dist/docs/recipes/create-user-interface.md +96 -0
- package/dist/docs/recipes/create-validator.md +67 -0
- package/dist/docs/recipes/create-waiting-room-ui.md +109 -0
- package/dist/docs/recipes/declare-api-uses.md +67 -0
- package/dist/docs/recipes/design-state-for-concurrent-writes.md +96 -0
- package/dist/docs/recipes/distribute-package.md +80 -0
- package/dist/docs/recipes/e2e-testing.md +116 -0
- package/dist/docs/recipes/end-game-on-condition.md +154 -0
- package/dist/docs/recipes/fix-duplicate-packages.md +101 -0
- package/dist/docs/recipes/index.md +86 -0
- package/dist/docs/recipes/make-an-endpoint-external.md +101 -0
- package/dist/docs/recipes/negotiate-across-teams.md +164 -0
- package/dist/docs/recipes/observe-other-teams-state.md +119 -0
- package/dist/docs/recipes/publish-a-package.md +139 -0
- package/dist/docs/recipes/remember-a-viewers-choice.md +55 -0
- package/dist/docs/recipes/review-a-past-day.md +73 -0
- package/dist/docs/recipes/share-assets-across-scenarios.md +120 -0
- package/dist/docs/recipes/share-logic-between-engine-and-ui.md +130 -0
- package/dist/docs/recipes/show-all-teams-participants.md +109 -0
- package/dist/docs/recipes/start-from-an-empty-package.md +169 -0
- package/dist/docs/recipes/take-a-file-in-an-endpoint.md +82 -0
- package/dist/docs/recipes/test-a-multi-role-game.md +51 -0
- package/dist/docs/recipes/test-user-interface.md +95 -0
- package/dist/docs/recipes/test-with-engine.md +115 -0
- package/dist/docs/recipes/upload-and-display-files.md +148 -0
- package/dist/docs/recipes/use-request-play.md +65 -0
- package/dist/docs/recipes/use-request-tick.md +75 -0
- package/dist/docs/troubleshooting.md +816 -0
- package/dist/execution/blob-inputs.d.ts +57 -0
- package/dist/execution/bundle-reload.d.ts +42 -0
- package/dist/execution/config-resolver.d.ts +127 -0
- package/dist/execution/configuration-builder.d.ts +66 -0
- package/dist/execution/context-composition.d.ts +161 -0
- package/dist/execution/endpoint-actions.d.ts +9 -0
- package/dist/execution/endpoint-payloads.d.ts +32 -0
- package/dist/execution/endpoint-runner.d.ts +129 -0
- package/dist/execution/engine-state.d.ts +40 -0
- package/dist/execution/executor-callbacks.d.ts +56 -0
- package/dist/execution/executor.d.ts +436 -0
- package/dist/execution/gates.d.ts +45 -0
- package/dist/execution/hook-chain.d.ts +25 -0
- package/dist/execution/logic-calls.d.ts +190 -0
- package/dist/execution/logic-failure.d.ts +84 -0
- package/dist/execution/logic-loader.d.ts +54 -0
- package/dist/execution/logic-result.d.ts +76 -0
- package/dist/execution/tick-controller-call.d.ts +32 -0
- package/dist/execution/types.d.ts +48 -0
- package/dist/generation/ts-type.d.ts +39 -0
- package/dist/guide/locale.d.ts +12 -0
- package/dist/guide/video-registry.d.ts +42 -0
- package/dist/help-examples.d.ts +125 -0
- package/dist/host/adapter.d.ts +67 -0
- package/dist/host/host-channel.d.ts +222 -0
- package/dist/host/index.d.ts +20 -0
- package/dist/host/protocol.d.ts +783 -0
- package/dist/host/surfaces.d.ts +42 -0
- package/dist/host/theme.d.ts +55 -0
- package/dist/host.js +583 -0
- package/dist/prompts/index.d.ts +7 -0
- package/dist/prompts/schema-builder.d.ts +29 -0
- package/dist/prompts/types.d.ts +108 -0
- package/dist/prompts/validators.d.ts +32 -0
- package/dist/runtime/capability-scope.d.ts +6 -0
- package/dist/runtime/context-merge.d.ts +29 -0
- package/dist/runtime/context-projection.d.ts +41 -0
- package/dist/runtime/guarded-callbacks.d.ts +15 -0
- package/dist/runtime/index.d.ts +27 -0
- package/dist/runtime/manifest-version.d.ts +55 -0
- package/dist/runtime/tick-validation.d.ts +25 -0
- package/dist/runtime.js +346 -0
- package/dist/schema.json +1546 -0
- package/dist/server/vite-plugin.d.ts +46 -0
- package/dist/services/tts/client.d.ts +51 -0
- package/dist/services/tts/elevenlabs.d.ts +24 -0
- package/dist/services/tts/types.d.ts +44 -0
- package/dist/storage/asset-resolver.d.ts +49 -0
- package/dist/storage/blob-store.d.ts +41 -0
- package/dist/templates/endpoint/logic.test.ts.liquid +61 -0
- package/dist/templates/endpoint/logic.ts.liquid +25 -0
- package/dist/templates/engine/init.json.liquid +7 -0
- package/dist/templates/engine/logic-aggregate.test.ts.liquid +90 -0
- package/dist/templates/engine/logic-aggregate.ts.liquid +46 -0
- package/dist/templates/engine/logic.test.ts.liquid +61 -0
- package/dist/templates/engine/logic.ts.liquid +32 -0
- package/dist/templates/engine/run.json.liquid +7 -0
- package/dist/templates/guide/01-at-a-glance.md.liquid +19 -0
- package/dist/templates/guide/02-preparation.md.liquid +18 -0
- package/dist/templates/guide/03-running-the-session.md.liquid +37 -0
- package/dist/templates/hook/logic.test.ts.liquid +46 -0
- package/dist/templates/hook/logic.ts.liquid +12 -0
- package/dist/templates/package/CLAUDE.md.template +338 -0
- package/dist/templates/package/DESIGN.md.liquid +144 -0
- package/dist/templates/package/README.md.liquid +143 -0
- package/dist/templates/package/claude/rules/workflow-create-capability.md +26 -0
- package/dist/templates/package/claude/rules/workflow-create-endpoint.md +35 -0
- package/dist/templates/package/claude/rules/workflow-create-engine.md +35 -0
- package/dist/templates/package/claude/rules/workflow-create-guide.md +40 -0
- package/dist/templates/package/claude/rules/workflow-create-presentation.md +37 -0
- package/dist/templates/package/claude/rules/workflow-create-user-interface.md +31 -0
- package/dist/templates/package/claude/rules/workflow-publish-package.md +54 -0
- package/dist/templates/package/claude/rules/workflow-test-engine-or-ui.md +30 -0
- package/dist/templates/package/claude/rules/workflow-update-sdk-version.md +43 -0
- package/dist/templates/package/claude/settings.json.template +16 -0
- package/dist/templates/package/gitignore.template +15 -0
- package/dist/templates/package/global-setup.ts.template +21 -0
- package/dist/templates/package/globals.d.ts.template +2 -0
- package/dist/templates/package/oxfmtrc.json.template +14 -0
- package/dist/templates/package/oxlintrc.json.template +238 -0
- package/dist/templates/package/package.json.liquid +65 -0
- package/dist/templates/package/playwright.config.ts.liquid +19 -0
- package/dist/templates/package/pnpm-workspace.yaml.template +27 -0
- package/dist/templates/package/test-setup.ts.liquid +7 -0
- package/dist/templates/package/tsconfig.json.liquid +15 -0
- package/dist/templates/package/vite.config.ts.liquid +19 -0
- package/dist/templates/package/vitest.config.ts.liquid +25 -0
- package/dist/templates/package/vitest.engine.config.ts.liquid +12 -0
- package/dist/templates/presentation/index.html.liquid +20 -0
- package/dist/templates/presentation/src/App.tsx.liquid +49 -0
- package/dist/templates/presentation/src/Slides.tsx.liquid +81 -0
- package/dist/templates/presentation/src/i18n.ts.liquid +54 -0
- package/dist/templates/presentation/src/main.tsx.liquid +14 -0
- package/dist/templates/scenario_editor/src/App.test.tsx.liquid +30 -0
- package/dist/templates/scenario_editor/src/App.tsx.liquid +95 -0
- package/dist/templates/scenario_editor/src/i18n.ts.liquid +44 -0
- package/dist/templates/session/src/App.test.tsx.liquid +60 -0
- package/dist/templates/session/src/App.tsx.liquid +128 -0
- package/dist/templates/session/src/i18n.ts.liquid +53 -0
- package/dist/templates/session/src/main.tsx.liquid +48 -0
- package/dist/templates/session/src/theme.ts.liquid +46 -0
- package/dist/templates/settings-editor/index.html.liquid +12 -0
- package/dist/templates/settings-editor/src/main.tsx.liquid +48 -0
- package/dist/templates/settings-editor/src/theme.ts.liquid +24 -0
- package/dist/templates/setup/src/App.test.tsx.liquid +39 -0
- package/dist/templates/setup/src/App.tsx.liquid +102 -0
- package/dist/templates/setup/src/i18n.ts.liquid +47 -0
- package/dist/templates/starter-game/capabilities/briefing/capability.sdk.json +18 -0
- package/dist/templates/starter-game/capabilities/plan/capability.sdk.json +17 -0
- package/dist/templates/starter-game/capabilities/results/capability.sdk.json +61 -0
- package/dist/templates/starter-game/capabilities/roles/capability.sdk.json +17 -0
- package/dist/templates/starter-game/capabilities/weather/capability.sdk.json +23 -0
- package/dist/templates/starter-game/engine/defaults.ts +81 -0
- package/dist/templates/starter-game/engine/engine.sdk.json +12 -0
- package/dist/templates/starter-game/engine/logic.test.ts +221 -0
- package/dist/templates/starter-game/engine/logic.ts +149 -0
- package/dist/templates/starter-game/engine/test-helpers.ts +74 -0
- package/dist/templates/starter-game/engine/types.ts +139 -0
- package/dist/templates/starter-game/guide/content/en/01-at-a-glance.md +51 -0
- package/dist/templates/starter-game/guide/content/en/02-preparation.md +35 -0
- package/dist/templates/starter-game/guide/content/en/03-running-the-day.md +58 -0
- package/dist/templates/starter-game/guide/guide.sdk.json +10 -0
- package/dist/templates/starter-game/guide/guide.test.ts +108 -0
- package/dist/templates/starter-game/hooks/assignproducts/defaults.ts +37 -0
- package/dist/templates/starter-game/hooks/assignproducts/hook.sdk.json +13 -0
- package/dist/templates/starter-game/hooks/assignproducts/logic/logic.test.ts +77 -0
- package/dist/templates/starter-game/hooks/assignproducts/logic/logic.ts +47 -0
- package/dist/templates/starter-game/hooks/assignproducts/types.ts +55 -0
- package/dist/templates/starter-game/package.json +68 -0
- package/dist/templates/starter-game/playwright.config.ts +19 -0
- package/dist/templates/starter-game/scenarios/heatwave/scenario.sdk.json +20 -0
- package/dist/templates/starter-game/scenarios/ordinary_week/scenario.sdk.json +12 -0
- package/dist/templates/starter-game/seriousgaming.sdk.json +23 -0
- package/dist/templates/starter-game/session/defaults.ts +80 -0
- package/dist/templates/starter-game/session/index.html +12 -0
- package/dist/templates/starter-game/session/session.sdk.json +11 -0
- package/dist/templates/starter-game/session/src/App.test.tsx +307 -0
- package/dist/templates/starter-game/session/src/App.tsx +309 -0
- package/dist/templates/starter-game/session/src/Section.tsx +37 -0
- package/dist/templates/starter-game/session/src/TeamDoor.tsx +96 -0
- package/dist/templates/starter-game/session/src/format.ts +8 -0
- package/dist/templates/starter-game/session/src/i18n.ts +51 -0
- package/dist/templates/starter-game/session/src/main.tsx +48 -0
- package/dist/templates/starter-game/session/src/theme.ts +45 -0
- package/dist/templates/starter-game/session/types.ts +135 -0
- package/dist/templates/starter-game/settings/engine/settings.sdk.json +50 -0
- package/dist/templates/starter-game/settings/menu/settings.sdk.json +78 -0
- package/dist/templates/starter-game/settings/session/settings.sdk.json +23 -0
- package/dist/templates/starter-game/settings/ui/settings.sdk.json +25 -0
- package/dist/templates/starter-game/test-setup.ts +7 -0
- package/dist/templates/starter-game/testing/e2e/game.spec.ts +139 -0
- package/dist/templates/starter-game/testing/e2e/global-setup.ts +21 -0
- package/dist/templates/starter-game/testing/engine/game.engine.test.ts +171 -0
- package/dist/templates/starter-game/tsconfig.json +19 -0
- package/dist/templates/starter-game/ui/defaults.ts +60 -0
- package/dist/templates/starter-game/ui/index.html +12 -0
- package/dist/templates/starter-game/ui/src/App.test.tsx +138 -0
- package/dist/templates/starter-game/ui/src/App.tsx +175 -0
- package/dist/templates/starter-game/ui/src/DayBar.test.tsx +78 -0
- package/dist/templates/starter-game/ui/src/DayBar.tsx +72 -0
- package/dist/templates/starter-game/ui/src/HistoryPanel.test.tsx +74 -0
- package/dist/templates/starter-game/ui/src/HistoryPanel.tsx +120 -0
- package/dist/templates/starter-game/ui/src/MakeRow.tsx +129 -0
- package/dist/templates/starter-game/ui/src/Section.tsx +37 -0
- package/dist/templates/starter-game/ui/src/ThemeToggle.tsx +49 -0
- package/dist/templates/starter-game/ui/src/TodayPanel.test.tsx +179 -0
- package/dist/templates/starter-game/ui/src/TodayPanel.tsx +92 -0
- package/dist/templates/starter-game/ui/src/YesterdayPanel.tsx +142 -0
- package/dist/templates/starter-game/ui/src/format.ts +12 -0
- package/dist/templates/starter-game/ui/src/i18n.ts +95 -0
- package/dist/templates/starter-game/ui/src/main.tsx +48 -0
- package/dist/templates/starter-game/ui/src/menu.ts +18 -0
- package/dist/templates/starter-game/ui/src/test-helpers.ts +58 -0
- package/dist/templates/starter-game/ui/src/theme.ts +45 -0
- package/dist/templates/starter-game/ui/src/writes.ts +43 -0
- package/dist/templates/starter-game/ui/types.ts +137 -0
- package/dist/templates/starter-game/ui/ui.sdk.json +11 -0
- package/dist/templates/starter-game/validators/capacity/defaults.ts +37 -0
- package/dist/templates/starter-game/validators/capacity/logic/logic.test.ts +96 -0
- package/dist/templates/starter-game/validators/capacity/logic/logic.ts +45 -0
- package/dist/templates/starter-game/validators/capacity/types.ts +55 -0
- package/dist/templates/starter-game/validators/capacity/validator.sdk.json +11 -0
- package/dist/templates/starter-game/vite.config.ts +19 -0
- package/dist/templates/starter-game/vitest.config.ts +15 -0
- package/dist/templates/starter-game/vitest.engine.config.ts +19 -0
- package/dist/templates/starter-game/waiting_room/defaults.ts +40 -0
- package/dist/templates/starter-game/waiting_room/index.html +12 -0
- package/dist/templates/starter-game/waiting_room/src/App.test.tsx +102 -0
- package/dist/templates/starter-game/waiting_room/src/App.tsx +194 -0
- package/dist/templates/starter-game/waiting_room/src/i18n.ts +33 -0
- package/dist/templates/starter-game/waiting_room/src/main.tsx +48 -0
- package/dist/templates/starter-game/waiting_room/src/theme.ts +45 -0
- package/dist/templates/starter-game/waiting_room/types.ts +59 -0
- package/dist/templates/starter-game/waiting_room/waiting_room.sdk.json +10 -0
- package/dist/templates/tick-controller/logic.ts.liquid +8 -0
- package/dist/templates/ui/index.html.liquid +12 -0
- package/dist/templates/ui/src/App.test.tsx.liquid +41 -0
- package/dist/templates/ui/src/App.tsx.liquid +103 -0
- package/dist/templates/ui/src/i18n.ts.liquid +47 -0
- package/dist/templates/ui/src/main.tsx.liquid +48 -0
- package/dist/templates/ui/src/theme.ts.liquid +46 -0
- package/dist/templates/validator/logic.test.ts.liquid +42 -0
- package/dist/templates/validator/logic.ts.liquid +21 -0
- package/dist/templates/waiting-room/index.html.liquid +12 -0
- package/dist/templates/waiting-room/src/App.test.tsx.liquid +57 -0
- package/dist/templates/waiting-room/src/App.tsx.liquid +118 -0
- package/dist/templates/waiting-room/src/i18n.ts.liquid +44 -0
- package/dist/templates/waiting-room/src/main.tsx.liquid +48 -0
- package/dist/templates/waiting-room/src/theme.ts.liquid +46 -0
- package/dist/templating/resource-metadata.d.ts +131 -0
- package/dist/types.d.ts +14 -0
- package/dist/utils/atomic-write.d.ts +40 -0
- package/dist/utils/cli-error.d.ts +64 -0
- package/dist/utils/document-parser-client.d.ts +20 -0
- package/dist/utils/env-loader.d.ts +40 -0
- package/dist/utils/error-codes.d.ts +335 -0
- package/dist/utils/errors.d.ts +19 -0
- package/dist/utils/global-settings.d.ts +65 -0
- package/dist/utils/llm-client.d.ts +34 -0
- package/dist/utils/logger.d.ts +36 -0
- package/dist/utils/output.d.ts +109 -0
- package/dist/utils/platform.d.ts +14 -0
- package/dist/utils/prompt.d.ts +36 -0
- package/dist/utils/real-path.d.ts +6 -0
- package/dist/utils/schema-properties.d.ts +35 -0
- package/dist/validation/ajv.d.ts +17 -0
- package/dist/validation/facilitator-guide-validator.d.ts +27 -0
- package/dist/validation/findings.d.ts +40 -0
- package/dist/validation/game-constraints.d.ts +10 -0
- package/dist/validation/game-validator.d.ts +34 -0
- package/dist/validation/package-validation.d.ts +41 -0
- package/dist/validation/package-validator.d.ts +19 -0
- package/dist/validation/resource-validator.d.ts +79 -0
- package/dist/validation/schema-defaults-validator.d.ts +29 -0
- package/dist/validation/schema-validation.d.ts +115 -0
- package/dist/validation/semver.d.ts +9 -0
- package/package.json +122 -3
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
This guide shows you how to write and run tests for your engine and user interfaces.
|
|
4
|
+
|
|
5
|
+
## What you'll learn
|
|
6
|
+
|
|
7
|
+
- How to write unit tests for the engine
|
|
8
|
+
- How to test React user interface components
|
|
9
|
+
- How to run tests locally
|
|
10
|
+
|
|
11
|
+
## Testing engine logic
|
|
12
|
+
|
|
13
|
+
### Starting from the generated defaults
|
|
14
|
+
|
|
15
|
+
Alongside `types.ts`, the build generates a `defaults.ts` beside every resource, holding the values a session opens with:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// engine/defaults.ts (generated, do not edit)
|
|
19
|
+
export const defaultContext: Context = {
|
|
20
|
+
inventory: { Stock: 0, LastRestocked: '' },
|
|
21
|
+
sales: { Sales: 0 },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const defaultSettings: Settings = {
|
|
25
|
+
engine: { DailySales: 10 },
|
|
26
|
+
};
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
A test spreads those and names only what the case is about:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const context: Context = {
|
|
33
|
+
...defaultContext,
|
|
34
|
+
inventory: { Stock: 100, LastRestocked: '2024-01-01' },
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This matters as an engine grows. `Context` is the union of every capability the engine reads or writes, and TypeScript requires all of them, so a test about one capability would otherwise have to write out twenty. The usual escape is `as unknown as Context`, which switches off shape checking for the whole fixture, so the test stops catching the typo it existed to catch. Spreading the defaults keeps both the brevity and the checking, and a capability added later arrives in every fixture already populated instead of breaking each one.
|
|
39
|
+
|
|
40
|
+
Because they are generated from the same schemas the types are, the values cannot drift from what a real session starts with.
|
|
41
|
+
|
|
42
|
+
An engine's `defaults.ts` also exports `defaultTeamContext` and `defaultSharedContext`, the same values split by scope to match the generated `TeamContext` and `SharedContext`. They are what an aggregate function's `teams` and `shared` arguments hold (see [Testing an aggregate function](#testing-an-aggregate-function)).
|
|
43
|
+
|
|
44
|
+
Every capability variable must declare a `default` for this to work. The build says so, naming the capability and the variable, if one does not.
|
|
45
|
+
|
|
46
|
+
### Testing the Run function
|
|
47
|
+
|
|
48
|
+
`Init` and `Run` receive a single args object `{ context, settings, meta, participants, callbacks }`. Most of it is the runtime's to supply, so build it with `logicArgs` from `@seriousgaming/sdk/testing` and name only what the test is about:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// engine/logic.test.ts
|
|
52
|
+
import { logicArgs } from '@seriousgaming/sdk/testing';
|
|
53
|
+
import { describe, it, expect } from 'vitest';
|
|
54
|
+
import { defaultSettings, defaultContext } from './defaults';
|
|
55
|
+
import { Run } from './logic';
|
|
56
|
+
import type { Context, Settings } from './types';
|
|
57
|
+
|
|
58
|
+
describe('Warehouse Engine', () => {
|
|
59
|
+
it('should decrease stock by daily sales', async () => {
|
|
60
|
+
const context: Context = {
|
|
61
|
+
...defaultContext,
|
|
62
|
+
inventory: { Stock: 100, LastRestocked: '2024-01-01' },
|
|
63
|
+
};
|
|
64
|
+
const settings: Settings = { engine: { ...defaultSettings.engine, DailySales: 10 } };
|
|
65
|
+
|
|
66
|
+
const result = await Run(logicArgs.run({ context, settings }));
|
|
67
|
+
|
|
68
|
+
expect(result.capabilities.inventory.Stock).toBe(90);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should not allow negative stock', async () => {
|
|
72
|
+
const context: Context = {
|
|
73
|
+
...defaultContext,
|
|
74
|
+
inventory: { Stock: 5, LastRestocked: '2024-01-01' },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const result = await Run(logicArgs.run({ context, settings: defaultSettings }));
|
|
78
|
+
|
|
79
|
+
expect(result.capabilities.inventory.Stock).toBeGreaterThanOrEqual(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The result passes straight to `Run` with no cast: its types come from `Run`'s own, which come from the generated `types.ts`. Everything not named is filled with fixed values:
|
|
85
|
+
|
|
86
|
+
| Argument | Default |
|
|
87
|
+
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
88
|
+
| `meta` | `tick: 1` (`0` for `logicArgs.init`), `teamId: 'team-1'`, `index: 0`, `locale: 'en'`, `seed: 'test-seed'`, `runKind: 'session'`, `now` 2026-01-01T00:00:00Z. Pass any of them in `meta` to change it |
|
|
89
|
+
| `participants` | One connected participant, id `'1'`, on the call's team |
|
|
90
|
+
| `callbacks` | The offline stubs (see [Testing with LLM callbacks](#testing-with-llm-callbacks)). Pass any of them in `callbacks` to replace it |
|
|
91
|
+
| `settings` | None. There is no runtime to resolve them, so pass the generated `defaultSettings`, spread with what the test changes. A resource that declares no settings may leave it out |
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
const args = logicArgs.run({
|
|
95
|
+
context,
|
|
96
|
+
settings: defaultSettings,
|
|
97
|
+
meta: { tick: 3, runKind: 'walkthrough' },
|
|
98
|
+
state: { BestProfit: 100 },
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A step function an engine calls from `Run` takes the same arguments, so `logicArgs.run` builds them too.
|
|
103
|
+
|
|
104
|
+
### Testing the Init function
|
|
105
|
+
|
|
106
|
+
`logicArgs.init` is `logicArgs.run` with `meta.tick` at 0, as `Init` sees it:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { Init } from './logic';
|
|
110
|
+
|
|
111
|
+
describe('Warehouse Init', () => {
|
|
112
|
+
it('should initialize with correct default values', async () => {
|
|
113
|
+
const settings: Settings = { engine: { ...defaultSettings.engine, InitialStock: 500 } };
|
|
114
|
+
|
|
115
|
+
const result = await Init(logicArgs.init({ context: defaultContext, settings }));
|
|
116
|
+
|
|
117
|
+
expect(result.capabilities.warehouse.Stock).toBe(500);
|
|
118
|
+
expect(result.capabilities.warehouse.ReorderCount).toBe(0);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Testing an aggregate function
|
|
124
|
+
|
|
125
|
+
`InitAggregate` and `RunAggregate` receive `{ shared, teams, settings, meta, participants, callbacks }`. Pass each team from `defaultTeamContext`, which carries team scope only, and the shared store once, in `shared`. `logicArgs.aggregate` names each team from its `index` and seats one participant on each:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { logicArgs } from '@seriousgaming/sdk/testing';
|
|
129
|
+
import { defaultSettings, defaultSharedContext, defaultTeamContext } from './defaults';
|
|
130
|
+
import { RunAggregate } from './logic';
|
|
131
|
+
|
|
132
|
+
function makeTeams(...prices: number[]) {
|
|
133
|
+
return prices.map((price, index) => ({
|
|
134
|
+
index,
|
|
135
|
+
context: { ...defaultTeamContext, price: { Price: price } },
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
it('writes the average price to the shared store', async () => {
|
|
140
|
+
const result = await RunAggregate(
|
|
141
|
+
logicArgs.aggregate({
|
|
142
|
+
shared: { context: defaultSharedContext },
|
|
143
|
+
teams: makeTeams(4, 6),
|
|
144
|
+
settings: defaultSettings,
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(result.shared.capabilities.market?.AverageMarketPrice).toBe(5);
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
For `InitAggregate`, pass `meta: { tick: 0 }`. A shared capability belongs in `result.shared.capabilities`, never in a `teams` entry: returning one there fails the tick at runtime, so a test that asserts on `result.teams[0].capabilities.market` is asserting on something the runtime would reject.
|
|
153
|
+
|
|
154
|
+
### Testing hooks, endpoints, validators and tick controllers
|
|
155
|
+
|
|
156
|
+
Each kind of logic has its own builder, typed from the function it is passed to:
|
|
157
|
+
|
|
158
|
+
| Builder | For | Fills, beyond `meta`, `participants` and `callbacks` |
|
|
159
|
+
| -------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
160
|
+
| `logicArgs.hook` | A hook's `Run` | The roster is the payload's `participant`. The signal defaults to `participantJoin`; name `signal: 'participantMove'` for a move, whose payload it then requires |
|
|
161
|
+
| `logicArgs.endpoint` | An endpoint's `Handle` or `Plan` | `team` (`team-1`), `session` (independent, no ticks completed) and `caller`: the team's first participant through `ui`, or pass `caller: { role: 'facilitator' }` (from `session`) or `caller: { role: 'author' }` (from `scenario_editor`, with no `sessionId`) |
|
|
162
|
+
| `logicArgs.validator` | A validator's `UpdateData` | `actor`: the roster's first participant |
|
|
163
|
+
| `logicArgs.requestTick` | A validator's `RequestTick` | `actor`, and `requestedTick: 1` |
|
|
164
|
+
| `logicArgs.tickController` | `GetTickController` | `teams`: the one team `meta` names, with `context`. `waitingRoomOpen: false` |
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const result = await Handle(
|
|
168
|
+
logicArgs.endpoint({ request: { price: 12 }, context, caller: { role: 'facilitator' } })
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const verdict = await UpdateData(
|
|
172
|
+
logicArgs.validator({ previous, next, settings: defaultSettings })
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
await Run(
|
|
176
|
+
logicArgs.hook({
|
|
177
|
+
signal: 'participantMove',
|
|
178
|
+
context,
|
|
179
|
+
participant,
|
|
180
|
+
fromTeamId: 'team-1',
|
|
181
|
+
toTeamId: 'team-2',
|
|
182
|
+
phase: 'departure',
|
|
183
|
+
})
|
|
184
|
+
);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Testing with LLM callbacks
|
|
188
|
+
|
|
189
|
+
The callbacks `logicArgs` fills are offline stubs, the same a test session uses; pass one in `callbacks` to choose its answer. See [Service callbacks are stubbed](./engine-testing.md#service-callbacks-are-stubbed) for what each stub answers and how to override it.
|
|
190
|
+
|
|
191
|
+
## Testing user interfaces
|
|
192
|
+
|
|
193
|
+
User interfaces are React components. The SDK scaffold sets up [Vitest](https://vitest.dev/) with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) automatically: `App.test.tsx` is created alongside `App.tsx`.
|
|
194
|
+
|
|
195
|
+
### How it works
|
|
196
|
+
|
|
197
|
+
A package is one game, so it has one of each of these at its root:
|
|
198
|
+
|
|
199
|
+
- `vitest.config.ts`: Vitest config with `jsdom` environment
|
|
200
|
+
- `test-setup.ts`: imports `@testing-library/jest-dom` matchers and sets up cleanup
|
|
201
|
+
|
|
202
|
+
Each UI's tests sit beside its component: the game UI's are at `ui/src/App.test.tsx`.
|
|
203
|
+
|
|
204
|
+
### Using render helpers
|
|
205
|
+
|
|
206
|
+
The SDK provides render helpers that set up all required context providers with sensible defaults. Each UI type has a dedicated helper:
|
|
207
|
+
|
|
208
|
+
| Helper | UI type |
|
|
209
|
+
| ------------------------------ | ----------------------------- |
|
|
210
|
+
| `renderUserInterface()` | Game UIs |
|
|
211
|
+
| `renderSettingsEditor()` | Setup and the scenario editor |
|
|
212
|
+
| `renderSessionUserInterface()` | Session UIs |
|
|
213
|
+
|
|
214
|
+
Every helper accepts an options object for overriding any context value and returns the `@testing-library/react` render result plus any mock functions (`setData`, a settings editor's `updateSettings`, `callbacks`) for assertions.
|
|
215
|
+
|
|
216
|
+
#### Game UIs
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
// ui/src/App.test.tsx
|
|
220
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
221
|
+
import { screen, fireEvent } from '@testing-library/react';
|
|
222
|
+
import { renderUserInterface } from '@seriousgaming/sdk/ui/testing';
|
|
223
|
+
import type { Context, Settings } from '../types';
|
|
224
|
+
import App from './App';
|
|
225
|
+
|
|
226
|
+
const mockData: Context = {
|
|
227
|
+
inventory: { Stock: 100 },
|
|
228
|
+
pricing: { Price: 25 },
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const mockSettings: Settings = { ui: { MaxPrice: 500 } };
|
|
232
|
+
|
|
233
|
+
describe('Dashboard', () => {
|
|
234
|
+
it('renders the current stock', () => {
|
|
235
|
+
renderUserInterface(<App />, { data: mockData, settings: mockSettings });
|
|
236
|
+
expect(screen.getByText(/100/)).toBeInTheDocument();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('calls setData when price is changed', () => {
|
|
240
|
+
const setData = vi.fn();
|
|
241
|
+
renderUserInterface(<App />, { data: mockData, settings: mockSettings, setData });
|
|
242
|
+
|
|
243
|
+
fireEvent.change(screen.getByLabelText(/price/i), { target: { value: '50' } });
|
|
244
|
+
expect(setData).toHaveBeenCalledWith({
|
|
245
|
+
pricing: { Price: 50 },
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('can request a tick via callbacks', () => {
|
|
250
|
+
const requestTick = vi.fn();
|
|
251
|
+
renderUserInterface(<App />, {
|
|
252
|
+
data: mockData,
|
|
253
|
+
settings: mockSettings,
|
|
254
|
+
callbacks: { requestTick },
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
screen.getByText('Submit').click();
|
|
258
|
+
expect(requestTick).toHaveBeenCalledOnce();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Default values when you omit an option:
|
|
264
|
+
|
|
265
|
+
| Option | Default |
|
|
266
|
+
| ---------------- | --------------------------------------------------------------------------------- |
|
|
267
|
+
| `data` | `{}` |
|
|
268
|
+
| `settings` | `{}` |
|
|
269
|
+
| `completedTicks` | `0` |
|
|
270
|
+
| `setData` | no-op |
|
|
271
|
+
| `theme` | `createMockTheme()` (light mode, MUI palette) |
|
|
272
|
+
| `locale` | `{ locale: 'en' }` |
|
|
273
|
+
| `participant` | `{ id: '1', name: 'Participant 1', index: 0, connected: true, teamId: 'team-1' }` |
|
|
274
|
+
| `participants` | `[]` (`createMockParticipants(n)` builds a roster) |
|
|
275
|
+
| `timer` | Disabled (no auto-tick) |
|
|
276
|
+
| `callbacks` | All present as no-ops |
|
|
277
|
+
|
|
278
|
+
Every option, and those of the other two helpers, is listed in the [UI testing API reference](../api/sdk-ui-testing.md#renderuserinterfaceoptions).
|
|
279
|
+
|
|
280
|
+
A waiting room renders under the game UI's providers, so `renderUserInterface` tests it too. A presentation has no render helper: see [Presentations](./presentation.md#testing).
|
|
281
|
+
|
|
282
|
+
#### Setup and the scenario editor
|
|
283
|
+
|
|
284
|
+
The generated `Settings` of a `setup` or `scenario_editor` slot is keyed by the settings resources it lists in `configures`, and every property of each is present, so its generated `defaultSettings` is what a fixture starts from. Writing the settings defaults out by hand instead leaves a copy that rots silently the next time the schema changes.
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { renderSettingsEditor } from '@seriousgaming/sdk/ui/testing';
|
|
288
|
+
import { defaultSettings } from './defaults';
|
|
289
|
+
|
|
290
|
+
it('calls updateSettings when a value changes', () => {
|
|
291
|
+
const updateSettings = vi.fn();
|
|
292
|
+
renderSettingsEditor(<App />, {
|
|
293
|
+
settings: { ...defaultSettings, engine: { ...defaultSettings.engine, BaseDemand: 100 } },
|
|
294
|
+
updateSettings,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
fireEvent.change(screen.getByLabelText(/base demand/i), { target: { value: '200' } });
|
|
298
|
+
expect(updateSettings).toHaveBeenCalledWith({ engine: { BaseDemand: 200 } });
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
#### Session UIs
|
|
303
|
+
|
|
304
|
+
Type the helpers with the surface's generated types: `renderSessionUserInterface<SessionData>`, `createMockTeamState<TeamContext>` and `createMockSharedState<SharedContext>`. Each fixture's context then accepts only capabilities of its own scope, any of them optional, so a shared capability left on a team's fixture fails to compile, as reading one through a team does in the surface. Left untyped, the helpers accept any context.
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import {
|
|
308
|
+
createMockSharedState,
|
|
309
|
+
createMockTeamState,
|
|
310
|
+
renderSessionUserInterface,
|
|
311
|
+
} from '@seriousgaming/sdk/ui/testing';
|
|
312
|
+
|
|
313
|
+
import type { SessionData, SharedContext, TeamContext } from '../types';
|
|
314
|
+
|
|
315
|
+
it('resets all teams', () => {
|
|
316
|
+
const resetGame = vi.fn();
|
|
317
|
+
renderSessionUserInterface<SessionData>(<SessionDashboard />, {
|
|
318
|
+
sessionData: {
|
|
319
|
+
// Shared capabilities arrive once, beside the teams. Either half
|
|
320
|
+
// may be left out, and defaults to empty
|
|
321
|
+
shared: createMockSharedState<SharedContext>({
|
|
322
|
+
context: { briefing: { Message: 'Welcome' } },
|
|
323
|
+
}),
|
|
324
|
+
teams: {
|
|
325
|
+
'team-1': createMockTeamState<TeamContext>({ completedTicks: 3 }),
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
teams: [{ teamId: 'team-1', index: 0, name: 'Alpha' }],
|
|
329
|
+
teamConfiguration: { mode: 'independent', max: 6 },
|
|
330
|
+
callbacks: { resetGame },
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
screen.getByText('Reset All').click();
|
|
334
|
+
expect(resetGame).toHaveBeenCalledOnce();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('writes a per-team override', () => {
|
|
338
|
+
const setData = vi.fn();
|
|
339
|
+
renderSessionUserInterface<SessionData>(<SessionDashboard />, {
|
|
340
|
+
sessionData: {
|
|
341
|
+
teams: {
|
|
342
|
+
'team-1': createMockTeamState<TeamContext>({ context: { price: { Price: 10 } } }),
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
teams: [{ teamId: 'team-1', index: 0, name: 'Alpha' }],
|
|
346
|
+
setData,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
fireEvent.change(screen.getByLabelText(/price/i), { target: { value: '20' } });
|
|
350
|
+
expect(setData).toHaveBeenCalledWith('team-1', { price: { Price: 20 } });
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
A session UI's `setData` takes the team id first for a team's own capabilities, and the update alone for a shared one. Either form passes its options last, so a delete is asserted as `expect(setData).toHaveBeenCalledWith('team-1', {}, { deletes: [['pinboard', 'Notes', 'p1']] })`.
|
|
355
|
+
|
|
356
|
+
### Never wire the providers by hand
|
|
357
|
+
|
|
358
|
+
Always render through these helpers. They supply every context a surface reads, with complete defaults, and each option overrides one value, so a test that needs a particular theme, locale, callback or viewer role passes it as an option. A hand-built provider tree has to supply every callback and every field itself, and it silently stops matching the runtime when the SDK adds one.
|
|
359
|
+
|
|
360
|
+
### Testing controlled inputs
|
|
361
|
+
|
|
362
|
+
For inputs that call `setData` on change, use `fireEvent.change` rather than `userEvent.type`. Because `setData` is mocked, the component's displayed value won't update between keystrokes. `fireEvent.change` fires a single synthetic event with the exact target value, which correctly triggers `onChange`:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { fireEvent } from '@testing-library/react';
|
|
366
|
+
|
|
367
|
+
it('calls setData when price is changed', () => {
|
|
368
|
+
const setData = vi.fn();
|
|
369
|
+
renderUserInterface(<App />, { data: mockData, settings: mockSettings, setData });
|
|
370
|
+
|
|
371
|
+
const priceInput = screen.getByLabelText(/price/i);
|
|
372
|
+
fireEvent.change(priceInput, { target: { value: '50' } });
|
|
373
|
+
|
|
374
|
+
expect(setData).toHaveBeenCalledWith({
|
|
375
|
+
pricing: { Price: 50 },
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Testing a stateful harness
|
|
381
|
+
|
|
382
|
+
Where a test keeps the context in local state so the component re-renders after each write, build `setData` with `mergingSetData` from `@seriousgaming/sdk/ui/testing`. It writes as the host does: objects merge recursively, arrays and scalars replace whole, and the paths in `deletes` are removed after the merge. A hand-rolled `{ ...previous, ...patch }` replaces whole capabilities, so a minimal write that is correct in the game fails the test:
|
|
383
|
+
|
|
384
|
+
```tsx
|
|
385
|
+
import { mergingSetData } from '@seriousgaming/sdk/ui/testing';
|
|
386
|
+
|
|
387
|
+
const [data, setLocal] = useState(mockData);
|
|
388
|
+
const setData = mergingSetData(setLocal);
|
|
389
|
+
|
|
390
|
+
setData({}, { deletes: ['pinboard.Notes.p1'] }); // the note's key is gone, not null
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Troubleshooting
|
|
394
|
+
|
|
395
|
+
**"Invalid hook call" or "Cannot read properties of null (reading 'useContext')"**
|
|
396
|
+
|
|
397
|
+
Two copies of React are loaded: one from your package and one from the `file://` SDK dependency. Add `resolve.dedupe` to your `vitest.config.ts`, with the SDK packages beside React so a second copy of the SDK cannot hide the providers either:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
export default defineConfig({
|
|
401
|
+
resolve: {
|
|
402
|
+
dedupe: [
|
|
403
|
+
'react',
|
|
404
|
+
'react-dom',
|
|
405
|
+
'@emotion/react',
|
|
406
|
+
'@emotion/styled',
|
|
407
|
+
'@seriousgaming/sdk',
|
|
408
|
+
'@seriousgaming/deck',
|
|
409
|
+
],
|
|
410
|
+
},
|
|
411
|
+
// ...
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
This is already included in the scaffold-generated `vitest.config.ts`.
|
|
416
|
+
|
|
417
|
+
**"Found multiple elements with the text: X" / DOM accumulation between tests**
|
|
418
|
+
|
|
419
|
+
React Testing Library cannot auto-detect Vitest's test lifecycle unless `globals: true` is set. Without it, the DOM is not cleaned up between tests. The scaffold-generated `test-setup.ts` handles this explicitly:
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
import { cleanup } from '@testing-library/react';
|
|
423
|
+
import { afterEach } from 'vitest';
|
|
424
|
+
|
|
425
|
+
afterEach(cleanup);
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
If you see stale elements from a previous test appearing in a later test, check that this is present in your `test-setup.ts`.
|
|
429
|
+
|
|
430
|
+
## Running tests
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
# Run all tests once
|
|
434
|
+
pnpm run test
|
|
435
|
+
|
|
436
|
+
# Watch mode
|
|
437
|
+
pnpm run test -- --watch
|
|
438
|
+
|
|
439
|
+
# Specific file
|
|
440
|
+
pnpm run test -- ui/src/App.test.tsx
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## The runtime your tests do not reproduce
|
|
444
|
+
|
|
445
|
+
An engine test imports `logic.ts` straight into Vitest, which runs under **Node**, with Node's full set of globals. Your game's production runtime is a **bare V8 isolate** with only the ECMAScript intrinsics (see [the language environment your logic runs in](./engine.md#the-language-environment-your-logic-runs-in)).
|
|
446
|
+
|
|
447
|
+
So a unit test cannot tell you that your logic is portable. This passes:
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
// logic.ts
|
|
451
|
+
export const Run: RunFunction<Context, Settings> = async ({ context }) => {
|
|
452
|
+
const snapshot = structuredClone(context); // works in Node, throws in production
|
|
453
|
+
// ...
|
|
454
|
+
};
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Its test goes green, because Node has `structuredClone`. The same tick throws `ReferenceError` once deployed.
|
|
458
|
+
|
|
459
|
+
Two things close the gap, and you should rely on both:
|
|
460
|
+
|
|
461
|
+
1. **Lint.** A scaffolded package restricts the absent globals inside the directories that hold logic (`engine/`, `tick_controller/`, and each hook's, validator's and endpoint's `logic/`), so `pnpm run lint` flags the reference before you run anything. This is the fastest signal and the one to trust. The rule is deliberately **off** for `*.test.ts` files, because a test legitimately runs in Node
|
|
462
|
+
2. **Exercise the logic through the real environment.** `pnpm run sdk dev` and `pnpm run sdk tick` both run logic in the restricted environment, so they reproduce the production failure. An [engine test](#engine-tests) drives the same execution path, which makes it a stronger portability signal than a direct unit test
|
|
463
|
+
|
|
464
|
+
The practical rule: unit-test your logic for correctness, and never treat a green unit test as evidence that it will run in production.
|
|
465
|
+
|
|
466
|
+
### Writes are checked when they happen
|
|
467
|
+
|
|
468
|
+
A unit test that calls `Init` or `Run` directly checks only what you assert. The runtime also checks each write against the declarations, and two of those checks see nothing until the write happens:
|
|
469
|
+
|
|
470
|
+
- **A capability outside `writes`.** An engine, aggregate or hook that changes a capability it only reads, or never declared, fails the call
|
|
471
|
+
- **A shared capability from a per-team `Init` or `Run`.** In an aggregate engine, only `InitAggregate` and `RunAggregate` may write shared state. A per-team `Init` that does stops the session starting
|
|
472
|
+
|
|
473
|
+
Both compare the value before and after the call, so a write that leaves the value as it was passes. An engine that changes a read-only capability only in round 4, or only under one scenario, fails there and nowhere else. `sdk build` does not run your logic, and a one-tick `sdk tick --no-server` passes every path that tick did not take.
|
|
474
|
+
|
|
475
|
+
So drive every code path that writes through the runtime: an [engine test](#engine-tests) that plays each round, each branch and each scenario, not one tick.
|
|
476
|
+
|
|
477
|
+
## Best practices
|
|
478
|
+
|
|
479
|
+
1. **Test what the user sees**: assert on rendered text, input values, and visible state rather than internal component state.
|
|
480
|
+
|
|
481
|
+
2. **Use `fireEvent.change` for controlled inputs**: `userEvent.type` won't work correctly when the mock `setData`/`updateSettings` doesn't update the displayed value between keystrokes.
|
|
482
|
+
|
|
483
|
+
3. **Keep context data close to realistic**: populate `mockData` with values that reflect a real game state. Tests that rely on fallback/undefined values hide bugs.
|
|
484
|
+
|
|
485
|
+
4. **Test edge cases**: zero values, maximum values, empty arrays, and missing optional capabilities.
|
|
486
|
+
|
|
487
|
+
5. **One `renderApp` helper per test file**: accept `dataOverrides` and `configOverrides` parameters so individual tests can vary only the data they care about.
|
|
488
|
+
|
|
489
|
+
## Engine tests
|
|
490
|
+
|
|
491
|
+
Unit tests check one function. An engine test runs the whole game in-process with `createTestSession` from `@seriousgaming/sdk-tools/client`: the session the dev server runs, with its hooks, validators, endpoints and timers, and offline stubs for the platform services. The [Engine testing](./engine-testing.md) guide is the reference: setup, the typed helpers, completion, endpoints, automatic ticks, and stubbing `generateText`.
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
pnpm test:engine
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
When a Playwright test fails, run the engine tests first. If one fails, the bug is in your logic; if they pass, it is in the UI or its connection to the host.
|
|
498
|
+
|
|
499
|
+
## Browser E2E tests (Playwright)
|
|
500
|
+
|
|
501
|
+
Unit and integration tests cover logic and components in isolation. Browser E2E tests verify that your game UI works correctly inside the dev server, including iframe embedding, tick progression, and the full interaction flow.
|
|
502
|
+
|
|
503
|
+
The SDK provides selectors and helpers via a dedicated subpath export with no Playwright dependency of its own:
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
import { expect, test } from '@playwright/test';
|
|
507
|
+
import { DEV_SERVER, reinitialiseSession } from '@seriousgaming/sdk/e2e';
|
|
508
|
+
|
|
509
|
+
test.beforeEach(async () => {
|
|
510
|
+
await reinitialiseSession(); // a fresh session from the package, started
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test('the value rises after a tick', async ({ page }) => {
|
|
514
|
+
// `?bare` drops the dev console's chrome, so this test is about the game.
|
|
515
|
+
await page.goto(DEV_SERVER.bare(DEV_SERVER.playerUrl(1)));
|
|
516
|
+
|
|
517
|
+
const frame = page.frameLocator(DEV_SERVER.iframeSelector('ui'));
|
|
518
|
+
await expect(frame.locator('#value')).toHaveText('0');
|
|
519
|
+
|
|
520
|
+
// Tick via the game's own control (the starter dashboard wires requestTick)
|
|
521
|
+
await frame.getByRole('button', { name: 'Next tick' }).click();
|
|
522
|
+
await expect(frame.locator('#value')).toHaveText('10');
|
|
523
|
+
});
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Anything a facilitator can do, a test can do without going through a surface:
|
|
527
|
+
`runCommand({ type: 'tick' })` advances the session, `getState()` reads the
|
|
528
|
+
whole projection, and `runCommand({ type: 'armFault', fault: 'conflict' })`
|
|
529
|
+
makes the 'Try again' path your UI offers reachable. `waitForState` waits for
|
|
530
|
+
something that happens after its command answers, and `advanceClock` moves the
|
|
531
|
+
time of a server started with a `FakeClock` on, so automatic ticks are tested without waiting.
|
|
532
|
+
|
|
533
|
+
When you create a project with `sdk new package`, a `playwright.config.ts`, the global setup that starts its dev server (`testing/e2e/global-setup.ts`, below) and a `testing/e2e/game.spec.ts` starter are generated automatically.
|
|
534
|
+
|
|
535
|
+
### A dev server for browser tests
|
|
536
|
+
|
|
537
|
+
`startDevServer` starts the dev server in your test runner's process, isolated from the one you are working in, and resolves once it answers. It is what `sdk dev --isolated --port auto` runs, without spawning the CLI and parsing its output:
|
|
538
|
+
|
|
539
|
+
- It takes free ports, so it starts beside your own dev server rather than asking you to stop it
|
|
540
|
+
- It writes no lock file, so `sdk tick` in your terminal still reaches your own server
|
|
541
|
+
- It reads configuration from the package alone, ignoring what you last ran `sdk configure` with
|
|
542
|
+
- Uploads go to a temporary directory, removed by `stop()`
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
// testing/e2e/global-setup.ts, as the scaffold writes it
|
|
546
|
+
import { dirname } from 'path';
|
|
547
|
+
|
|
548
|
+
import { startDevServer } from '@seriousgaming/sdk-tools/client';
|
|
549
|
+
import type { FullConfig } from '@playwright/test';
|
|
550
|
+
|
|
551
|
+
export default async function globalSetup(config: FullConfig): Promise<() => Promise<void>> {
|
|
552
|
+
const server = await startDevServer({
|
|
553
|
+
packagePath: config.configFile ? dirname(config.configFile) : process.cwd(),
|
|
554
|
+
});
|
|
555
|
+
process.env.SERIOUSGAMING_DEV_URL = server.url;
|
|
556
|
+
return () => server.stop();
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
`playwright.config.ts` names it as `globalSetup` and sets `use.baseURL` to `process.env.SERIOUSGAMING_DEV_URL`, which each worker reads after the setup has run. Stopping the server is a call in Playwright's own process, so the run ends when its tests do; a `webServer` running `pnpm run sdk dev` is stopped through `pnpm`, never reaches the server, and hangs.
|
|
561
|
+
|
|
562
|
+
It returns `{ url, assetUrl, stop() }`: `url` is the console and API, and `assetUrl` is where game surfaces are served from. The `@seriousgaming/sdk/e2e` helpers address the server `SERIOUSGAMING_DEV_URL` names, so a test passes them no options; pass `{ url: server.url }` where you hold the server yourself. It also takes `teams`, `participants`, `scenarioId`, `locale`, `clock` (a `FakeClock`) and `callbacks`, as [`createTestSession`](./engine-testing.md) does. Always call `stop()`: the server holds its ports and a file watcher open until then.
|
|
563
|
+
|
|
564
|
+
To drive it from the CLI as well, set `SERIOUSGAMING_DEV_URL` to its `url`.
|
|
565
|
+
|
|
566
|
+
See [Recipe: Test UIs end to end with Playwright](../recipes/e2e-testing.md) for the full setup guide, all available selectors, and common patterns.
|
|
567
|
+
|
|
568
|
+
## Next steps
|
|
569
|
+
|
|
570
|
+
- [Building user interfaces](./ui.md) - Build and develop custom UIs
|
|
571
|
+
- [Creating engines](./engine.md) - Write a testable engine
|
|
572
|
+
- [Engine testing](./engine-testing.md) - Whole-game tests with `createTestSession`
|
|
573
|
+
- [Recipe: Test a user interface](../recipes/test-user-interface.md) - Step-by-step testing walkthrough
|
|
574
|
+
- [Recipe: Test a whole game in-process](../recipes/test-with-engine.md) - In-process engine test setup
|
|
575
|
+
- [Recipe: Test UIs end to end with Playwright](../recipes/e2e-testing.md) - Browser-based end-to-end tests
|