Goal
We now have a working Playwright .NET e2e harness (issue #97, PR #110) and two smoke tests: boot-and-run (#97) and repeated-restart resilience (#90, PR #111). This issue widens coverage in one PR — pin the ~10-15 most important user-visible behaviors so a regression in any core path fails CI. Not aiming for 100% coverage; aiming for "confident the basics work."
Deliberately one PR (not many small ones) to keep review/merge overhead low. Reuse the e2e-testing skill patterns (publish → in-process Kestrel → headless Chromium + SwiftShader; observable-signal sync; null-and-wait).
This subsumes the planned #95 sample-switch guard — fold that test in here rather than opening a separate PR. (#90 already shipped in #111; #95 was still pending.)
Candidate tests
Grouped by area. Each notes what it pins, the signal to sync on, and what it needs. Implementer: pick the ~12 highest-value; if you cut any, say which and why (no silent drops). Foundation tests 0.a/0.b already exist.
0. Foundation (already in SmokeTest.cs)
A. Compile / run core — signal-driven, high confidence
- Compile error surfaces cleanly — type invalid C#, Run → diagnostics show failure,
#blazor-error-ui stays hidden, no game starts. Pins the Roslyn error path + graceful failure.
- Stop clears the running game — Run, then Stop → canvas cleared, status "Stopped.", Run button returns to "Run". Needs a testid on the Stop button.
- Edit-then-rerun recompiles vs cache-hit — Run (compiles → "Compiled in Xs"), Run again unchanged (cache hit → "Restarted without recompile"), edit code, Run (recompiles). Pins the
CompileFingerprint cache. Sync on the diagnostics <pre> text.
B. Examples / graphics profile — needs testids
4. Load a different example (A→B) — open Examples modal, pick another example → its code loads into Monaco and runs. Core of #95. Needs testids on the Examples button + modal cards.
5. Sample-switch round-trip A→B→A stays alive (full #95) — with a new GetInputDebugState app hook (exposes Mouse.GetState() position / touch count via JS interop) for a deterministic input assertion instead of a flaky pixel-read. This is the one place a new app-code hook is justified (see e2e-testing skill boundary).
6. Reach↔HiDef profile switch (issue #25 canvas-swap path — historically fragile) — all built-in examples are HiDef, so type a hand-written Reach game and a HiDef game and switch between them → _canvasContextType flips webgl↔webgl2, both run, no error banner.
C. Share / persistence — URL round-trip, browser-only wiring
7. #code= round-trip — Share writes the hash; reload at that URL → same code loads + runs. Round-trips UrlCodec through the real app.
8. #snippet= round-trip — same, through SnippetReverter/SnippetExpander. (Unit tests cover the codec; this pins the app wiring.)
9. Cold-boot deep link — navigate straight to a #code=<payload> URL → loads + runs with no manual paste.
D. Editor / IntelliSense — browser-only Roslyn, unit tests can't reach
10. Monaco boots with default code visible — editor mounted + non-empty. Cheap, high value.
11. IntelliSense completion returns results — prefer calling the completion JSInvokable via page.EvaluateAsync and asserting non-empty, rather than driving the Monaco widget (fragile). Pins the in-browser language service.
E. Embed mode
12. ?embed=1 → editor hidden, game runs, canvas fills. Pins embed layout + the embed FPS-cap path.
Optional / heavier (include only if wall-clock allows — flag if cut):
13. Load an asset via file input → appears in Assets and is usable by Content.Load. Drag-drop is hard to synthesize; use Playwright SetInputFilesAsync if there's a file <input> (verify).
14. Shader (.fx) example compiles + runs — downloads ~17 MB DXC wasm, slow. One test, tag with an NUnit [Category] so CI can exclude it if the e2e job balloons.
15. Export project → .zip download fires — assert the download event only; can't validate the zip builds in e2e. Lower value.
App-code changes this needs (call out in the PR)
data-testid attributes on: Stop button, Examples button + modal + cards, Share, Assets, embed canvas, diagnostics pane. Only run-button exists today. These are test hooks, not behavior.
- One debug hook —
GetInputDebugState JSInvokable (for test 5). Per the e2e-testing skill, add app hooks only for input/game-state assertions the DOM can't express.
Harness / infra
- Per-test isolation: the fixture currently shares one
IPage via OneTimeSetUp. With 10-15 tests, add a [SetUp] that gives each test a fresh page / re-navigation (keep browser + host shared in OneTimeSetUp — those are the expensive parts) so a still-running game or a set _canvasContextType can't leak across tests.
- CI stays windows-latest (KNI
XNA.Framework.Media case-sensitivity, see e2e.yml); dotnet test auto-discovers new tests — no workflow edit unless the shader test needs a category filter.
- Update the
e2e-testing skill with any new patterns discovered (per issue-workflow §7).
Acceptance criteria
Out of scope
Pixel-perfect render assertions; validating the exported zip actually builds; mobile/touch behavior (headless Chromium can't send real touch — that stays manual, per game-lifecycle).
Goal
We now have a working Playwright .NET e2e harness (issue #97, PR #110) and two smoke tests: boot-and-run (#97) and repeated-restart resilience (#90, PR #111). This issue widens coverage in one PR — pin the ~10-15 most important user-visible behaviors so a regression in any core path fails CI. Not aiming for 100% coverage; aiming for "confident the basics work."
Deliberately one PR (not many small ones) to keep review/merge overhead low. Reuse the
e2e-testingskill patterns (publish → in-process Kestrel → headless Chromium + SwiftShader; observable-signal sync; null-and-wait).This subsumes the planned #95 sample-switch guard — fold that test in here rather than opening a separate PR. (#90 already shipped in #111; #95 was still pending.)
Candidate tests
Grouped by area. Each notes what it pins, the signal to sync on, and what it needs. Implementer: pick the ~12 highest-value; if you cut any, say which and why (no silent drops). Foundation tests 0.a/0.b already exist.
0. Foundation (already in
SmokeTest.cs)window._canvasContextTypeset. (Add Playwright e2e smoke tests to CI (catch browser-only regressions like #90/#95) #97)A. Compile / run core — signal-driven, high confidence
#blazor-error-uistays hidden, no game starts. Pins the Roslyn error path + graceful failure.CompileFingerprintcache. Sync on the diagnostics<pre>text.B. Examples / graphics profile — needs testids
4. Load a different example (A→B) — open Examples modal, pick another example → its code loads into Monaco and runs. Core of #95. Needs testids on the Examples button + modal cards.
5. Sample-switch round-trip A→B→A stays alive (full #95) — with a new
GetInputDebugStateapp hook (exposesMouse.GetState()position / touch count via JS interop) for a deterministic input assertion instead of a flaky pixel-read. This is the one place a new app-code hook is justified (seee2e-testingskill boundary).6. Reach↔HiDef profile switch (issue #25 canvas-swap path — historically fragile) — all built-in examples are HiDef, so type a hand-written Reach game and a HiDef game and switch between them →
_canvasContextTypeflipswebgl↔webgl2, both run, no error banner.C. Share / persistence — URL round-trip, browser-only wiring
7.
#code=round-trip — Share writes the hash; reload at that URL → same code loads + runs. Round-tripsUrlCodecthrough the real app.8.
#snippet=round-trip — same, throughSnippetReverter/SnippetExpander. (Unit tests cover the codec; this pins the app wiring.)9. Cold-boot deep link — navigate straight to a
#code=<payload>URL → loads + runs with no manual paste.D. Editor / IntelliSense — browser-only Roslyn, unit tests can't reach
10. Monaco boots with default code visible — editor mounted + non-empty. Cheap, high value.
11. IntelliSense completion returns results — prefer calling the completion JSInvokable via
page.EvaluateAsyncand asserting non-empty, rather than driving the Monaco widget (fragile). Pins the in-browser language service.E. Embed mode
12.
?embed=1→ editor hidden, game runs, canvas fills. Pins embed layout + the embed FPS-cap path.Optional / heavier (include only if wall-clock allows — flag if cut):
13. Load an asset via file input → appears in Assets and is usable by
Content.Load. Drag-drop is hard to synthesize; use PlaywrightSetInputFilesAsyncif there's a file<input>(verify).14. Shader (.fx) example compiles + runs — downloads ~17 MB DXC wasm, slow. One test, tag with an NUnit
[Category]so CI can exclude it if the e2e job balloons.15. Export project →
.zipdownload fires — assert the download event only; can't validate the zip builds in e2e. Lower value.App-code changes this needs (call out in the PR)
data-testidattributes on: Stop button, Examples button + modal + cards, Share, Assets, embed canvas, diagnostics pane. Onlyrun-buttonexists today. These are test hooks, not behavior.GetInputDebugStateJSInvokable (for test 5). Per thee2e-testingskill, add app hooks only for input/game-state assertions the DOM can't express.Harness / infra
IPageviaOneTimeSetUp. With 10-15 tests, add a[SetUp]that gives each test a fresh page / re-navigation (keep browser + host shared inOneTimeSetUp— those are the expensive parts) so a still-running game or a set_canvasContextTypecan't leak across tests.XNA.Framework.Mediacase-sensitivity, seee2e.yml);dotnet testauto-discovers new tests — no workflow edit unless the shader test needs a category filter.e2e-testingskill with any new patterns discovered (per issue-workflow §7).Acceptance criteria
GetInputDebugStatedebug hook — no behavior changes.#95folded in and closed as superseded by this PR.e2e-testingskill updated if new patterns emerged.Out of scope
Pixel-perfect render assertions; validating the exported zip actually builds; mobile/touch behavior (headless Chromium can't send real touch — that stays manual, per game-lifecycle).