@@ -22,6 +22,8 @@ import {
2222} from "vitest" ;
2323import * as fs from "fs" ;
2424import App from "./App.svelte" ;
25+ import { data } from "./data" ;
26+ import { tileData } from "./tile-data" ;
2527import { dismiss , notifications } from "./Notification.svelte" ;
2628
2729vi . hoisted ( ( ) => {
@@ -86,7 +88,9 @@ const testModsData = {
8688
8789describe ( "Routing E2E Tests" , ( ) => {
8890 vi . setConfig ( { testTimeout : 120_000 } ) ;
91+ const ROUTING_TEARDOWN_SETTLE_MS = 150 ;
8992 let originalFetch : typeof global . fetch ;
93+ let originalImage : typeof global . Image ;
9094 let defaultFetchMock : typeof fetch ;
9195 let container : HTMLElement ;
9296
@@ -141,8 +145,30 @@ describe("Routing E2E Tests", () => {
141145
142146 beforeAll ( ( ) => {
143147 originalFetch = global . fetch ;
148+ originalImage = global . Image ;
144149 defaultFetchMock = createFetchMock ( ) ;
145150 global . fetch = defaultFetchMock ;
151+
152+ class MockImage {
153+ onload : ( ( this : GlobalEventHandlers , ev : Event ) => any ) | null = null ;
154+ onerror : OnErrorEventHandler = null ;
155+ width = 32 ;
156+ height = 32 ;
157+ #src = "" ;
158+
159+ get src ( ) {
160+ return this . #src;
161+ }
162+
163+ set src ( value : string ) {
164+ this . #src = value ;
165+ queueMicrotask ( ( ) => {
166+ this . onload ?. call ( window , new Event ( "load" ) ) ;
167+ } ) ;
168+ }
169+ }
170+
171+ global . Image = MockImage as unknown as typeof global . Image ;
146172 } ) ;
147173
148174 beforeEach ( ( ) => {
@@ -180,56 +206,41 @@ describe("Routing E2E Tests", () => {
180206 if ( container && container . parentNode ) {
181207 container . parentNode . removeChild ( container ) ;
182208 }
209+ data . _reset ( ) ;
210+ tileData . reset ( ) ;
183211 global . fetch = defaultFetchMock ;
184212 vi . clearAllMocks ( ) ;
185213 } ) ;
186214
187- afterAll ( ( ) => {
215+ afterAll ( async ( ) => {
216+ // Let pending Svelte window-binding timeouts settle before async-leak collection.
217+ await new Promise ( ( resolve ) =>
218+ setTimeout ( resolve , ROUTING_TEARDOWN_SETTLE_MS ) ,
219+ ) ;
188220 global . fetch = originalFetch ;
221+ global . Image = originalImage ;
189222 } ) ;
190223
191224 async function waitForDataLoad ( expectedText ?: string | RegExp ) {
192- const start = Date . now ( ) ;
193- const timeout = 10000 ;
225+ await waitFor ( ( ) => expect ( get ( data ) ) . not . toBeNull ( ) , { timeout : 10_000 } ) ;
226+ await waitFor (
227+ ( ) =>
228+ expect (
229+ document . querySelector ( ".loading-container.full-screen" ) ,
230+ ) . toBeNull ( ) ,
231+ { timeout : 10_000 } ,
232+ ) ;
194233
195- while ( Date . now ( ) - start < timeout ) {
196- const text = document . body . textContent || "" ;
197- const lowerText = text . toLowerCase ( ) ;
234+ if ( ! expectedText ) return ;
198235
199- if ( expectedText ) {
200- if ( typeof expectedText === "string" ) {
201- if ( lowerText . includes ( expectedText . toLowerCase ( ) ) ) return ;
202- } else if ( expectedText . test ( text ) ) {
203- return ;
204- }
205- } else {
206- const isLoading =
207- lowerText . includes ( "loading" ) &&
208- ( lowerText . includes ( "game data" ) || lowerText . includes ( "builds" ) ) ;
209- if ( ! isLoading ) {
210- if (
211- lowerText . includes ( "hitchhiker" ) ||
212- lowerText . includes ( "description" ) ||
213- lowerText . includes ( "results" ) ||
214- lowerText . includes ( "catalog" ) ||
215- lowerText . includes ( "items" ) ||
216- lowerText . includes ( "location" ) ||
217- lowerText . includes ( "monsters" ) ||
218- lowerText . includes ( "mutations" ) ||
219- lowerText . includes ( "rock" )
220- ) {
221- return ;
222- }
223- }
236+ await waitFor ( ( ) => {
237+ const text = document . body . textContent || "" ;
238+ if ( typeof expectedText === "string" ) {
239+ expect ( text . toLowerCase ( ) ) . toContain ( expectedText . toLowerCase ( ) ) ;
240+ return ;
224241 }
225- await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
226- }
227-
228- const currentLoc = window . location . pathname ;
229- const bodySlice = document . body . textContent ?. slice ( 0 , 1000 ) || "" ;
230- throw new Error (
231- `Timed out waiting for data load (expected: ${ expectedText || "any trigger" } ).\nURL: ${ currentLoc } \nBody snippet: ${ bodySlice } ` ,
232- ) ;
242+ expect ( expectedText . test ( text ) ) . toBe ( true ) ;
243+ } ) ;
233244 }
234245
235246 async function waitForNavigation ( ) {
@@ -349,6 +360,7 @@ describe("Routing E2E Tests", () => {
349360
350361 // Check query param persists
351362 expect ( window . location . search ) . toContain ( "lang=ru_RU" ) ;
363+ await waitForDataLoad ( ) ;
352364 } ) ;
353365
354366 test ( "navigateTo preserves mods query param" , async ( ) => {
@@ -386,6 +398,7 @@ describe("Routing E2E Tests", () => {
386398 expect ( new URL ( window . location . href ) . searchParams . get ( "mods" ) ) . toBe (
387399 "aftershock,magiclysm" ,
388400 ) ;
401+ await waitForDataLoad ( ) ;
389402 } ) ;
390403
391404 test ( "loads compatible mod tileset chunk URLs for active mods" , async ( ) => {
@@ -770,6 +783,7 @@ describe("Routing E2E Tests", () => {
770783 describe ( "Version Handling" , ( ) => {
771784 test ( "navigates to correct version with incorrect typed-in URL" , async ( ) => {
772785 const warnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } ) ;
786+ const setVersionSpy = vi . spyOn ( data , "setVersion" ) ;
773787 const originalReplace = window . location . replace ;
774788 let replaceCalled = false ;
775789 delete ( window . location as any ) . replace ;
@@ -793,8 +807,10 @@ describe("Routing E2E Tests", () => {
793807 // Should have called location.replace to prepend /stable/
794808 expect ( replaceCalled ) . toBe ( true ) ;
795809 expect ( window . location . pathname ) . toContain ( "stable/invalid-version-999" ) ;
810+ expect ( setVersionSpy ) . not . toHaveBeenCalled ( ) ;
796811
797812 warnSpy . mockRestore ( ) ;
813+ setVersionSpy . mockRestore ( ) ;
798814 window . location . replace = originalReplace ;
799815 } ) ;
800816
0 commit comments