Skip to content

Fix duplicate DOM elements from custom game JS on restart#1833

Merged
alexwarren merged 1 commit into
mainfrom
fix-wasmplayer-webplayer-restart-dom-reset
Jul 6, 2026
Merged

Fix duplicate DOM elements from custom game JS on restart#1833
alexwarren merged 1 commit into
mainfrom
fix-wasmplayer-webplayer-restart-dom-reset

Conversation

@alexwarren

Copy link
Copy Markdown
Contributor

Summary

  • "Start from the beginning" / Load reuses the live DOM instead of doing a fresh page load, but previously only ever cleared the output pane. Games commonly use their external <javascript> resources to inject elements elsewhere in the chrome (e.g. a custom sidebar pane), and that setup code re-runs on every restart (RegisterExternalScripts fires on every Initialise call, and a game's StartGame function can call JS.* directly too) — so anything the previous run injected was still there when the new run added another one.
  • WasmPlayer: restartGame() now rebuilds the whole player chrome via a new swapInPlayerUi() helper, same as the initial boot, instead of only resetting #divOutput. Also dedupes addExternalScript() by URL in the shared player.js so a script's top-level setup code doesn't re-execute across restarts.
  • WebPlayer: mirrors the same fix — Game.razor wraps its rendered chrome in #qv-player-chrome (Blazor never re-diffs it once rendered, since the underlying string is constant) and resets it via WebPlayer.resetPlayerUi() before a restart.

Test plan

  • dotnet build --configuration Release (full solution)
  • dotnet test --configuration Release (all projects pass)
  • Playwright regression test against WasmPlayer dev server (tests/e2e/verify-wasmplayer-restart-sidebar-dup.mjs + fixture game tests/e2e/fixtures/restart-test.aslx): confirms exactly 1 injected pane survives repeated restarts; confirmed the old code duplicates it (1 → 2 → 3) to validate the test is meaningful
  • Playwright regression test against a locally-run WebPlayer (tests/e2e/verify-webplayer-restart-chrome-reset.mjs): confirms a manually-injected chrome marker is wiped on restart and gameplay still works afterwards; confirmed the old code leaves the marker in place

🤖 Generated with Claude Code

"Start from the beginning" / Load reused the live DOM instead of a fresh
page load, but only ever cleared the output pane. Games commonly use their
external <javascript> resources to inject elements elsewhere in the chrome
(e.g. a custom sidebar pane), and those re-run their setup on every restart
(RegisterExternalScripts fires on every Initialise call, and a game's
StartGame function can also call JS.* directly) — so anything the previous
run injected was still sitting there when the new run added another one.

WasmPlayer: restartGame() now rebuilds the whole player chrome via a new
swapInPlayerUi() helper, same as the initial boot, instead of only resetting
#divOutput. Also dedupe addExternalScript() by URL in the shared player.js so
a script's top-level setup code doesn't re-execute at all across restarts.

WebPlayer: mirrors the same idea — Game.razor now wraps its rendered chrome
in #qv-player-chrome (Blazor never re-diffs it once rendered, since the
underlying string is constant) and resets it via WebPlayer.resetPlayerUi()
before a restart.

Verified with Playwright against both players: a reproducer fixture
(tests/e2e/fixtures/restart-test.aslx) confirms exactly one injected pane
survives repeated restarts, and confirms the old code duplicates it (1 → 2 →
3) to validate the regression tests are meaningful.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@alexwarren alexwarren merged commit 195764e into main Jul 6, 2026
3 checks passed
@alexwarren alexwarren deleted the fix-wasmplayer-webplayer-restart-dom-reset branch July 6, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant