@@ -7,6 +7,7 @@ import {fakeServer, type FakeServer} from 'nise';
77import { type Tile } from '../tile/tile' ;
88import { stubAjaxGetImage , waitForEvent } from '../util/test/util' ;
99import { type MapSourceDataEvent } from '../ui/events' ;
10+ import * as loadTileJSONModule from './load_tilejson' ;
1011
1112function createSource ( options , transformCallback ?) {
1213 const source = new RasterTileSource ( 'id' , options , { send ( ) { } } as any as Dispatcher , options . eventedParent ) ;
@@ -30,7 +31,7 @@ describe('RasterTileSource', () => {
3031 } ) ;
3132
3233 afterEach ( ( ) => {
33- server . restore ( ) ;
34+ vi . restoreAllMocks ( ) ;
3435 } ) ;
3536
3637 test ( 'transforms request for TileJSON URL' , ( ) => {
@@ -340,27 +341,174 @@ describe('RasterTileSource', () => {
340341 expect ( tile . state ) . toBe ( 'unloaded' ) ;
341342 } ) ;
342343
343- test ( 'setUrl aborts in-flight raster tile requests through TileManager' , ( ) => {
344+ test ( 'loads tile after previous abort flag was set' , async ( ) => {
345+ server . respondWith ( '/source.json' , JSON . stringify ( {
346+ minzoom : 0 ,
347+ maxzoom : 22 ,
348+ attribution : 'MapLibre' ,
349+ tiles : [ 'http://example.com/{z}/{x}/{y}.png' ] ,
350+ bounds : [ - 47 , - 7 , - 45 , - 5 ]
351+ } ) ) ;
352+ server . respondWith ( 'http://example.com/10/5/5.png' ,
353+ [ 200 , { 'Content-Type' : 'image/png' , 'Content-Length' : 1 } , '0' ]
354+ ) ;
355+
356+ const source = createSource ( { url : '/source.json' } ) ;
357+ source . map . painter = { context : { } , getTileTexture : ( ) => ( { update : ( ) => { } } ) } as any ;
358+
359+ const sourcePromise = waitForEvent ( source , 'data' , ( e : MapSourceDataEvent ) => e . sourceDataType === 'metadata' ) ;
360+ server . respond ( ) ;
361+ await sourcePromise ;
362+
363+ const tile = {
364+ tileID : new OverscaledTileID ( 10 , 0 , 10 , 5 , 5 ) ,
365+ state : 'loading' ,
366+ aborted : true ,
367+ setExpiryData ( ) { }
368+ } as any as Tile ;
369+
370+ const tilePromise = source . loadTile ( tile ) ;
371+ server . respond ( ) ;
372+ await tilePromise ;
373+
374+ expect ( tile . state ) . toBe ( 'loaded' ) ;
375+ expect ( tile . aborted ) . toBe ( false ) ;
376+ } ) ;
377+
378+ test ( 'setSourceProperty aborts in-flight TileJSON request, emits abortPendingTileRequests, then calls load(true)' , ( ) => {
344379 const source = createSource ( {
345380 tiles : [ 'http://example.com/{z}/{x}/{y}.png' ]
346381 } ) ;
347382
348- const abortAllRequests = vi . fn ( ) ;
383+ const requestAbort = vi . fn ( ) ;
384+ ( source as any ) . _tileJSONRequest = { abort : requestAbort } ;
349385
350- ( source . map as any ) . style = {
351- tileManagers : {
352- [ source . id ] : { abortAllRequests}
353- }
354- } ;
386+ const isAbortEvent = ( e : any ) => e ?. abortPendingTileRequests === true || e ?. data ?. abortPendingTileRequests === true ;
387+ const sequence : string [ ] = [ ] ;
388+
389+ source . on ( 'data' , ( e : any ) => {
390+ if ( isAbortEvent ( e ) ) sequence . push ( 'abort-event' ) ;
391+ } ) ;
392+
393+ const callback = vi . fn ( ( ) => sequence . push ( 'callback' ) ) ;
394+ const loadSpy = vi . spyOn ( source , 'load' ) . mockImplementation ( async ( ...args : any [ ] ) => {
395+ sequence . push ( 'load' ) ;
396+ expect ( args [ 0 ] ) . toBe ( true ) ;
397+ return undefined as any ;
398+ } ) ;
399+
400+ source . setSourceProperty ( callback ) ;
401+
402+ expect ( requestAbort ) . toHaveBeenCalledTimes ( 1 ) ;
403+ expect ( ( source as any ) . _tileJSONRequest ) . toBeNull ( ) ;
404+ expect ( callback ) . toHaveBeenCalledTimes ( 1 ) ;
405+ expect ( loadSpy ) . toHaveBeenCalledTimes ( 1 ) ;
406+ expect ( loadSpy ) . toHaveBeenCalledWith ( true ) ;
407+ expect ( sequence ) . toEqual ( [ 'abort-event' , 'callback' , 'load' ] ) ;
408+ } ) ;
409+
410+ test ( 'setSourceProperty emits abortPendingTileRequests even without in-flight TileJSON request' , ( ) => {
411+ const source = createSource ( {
412+ tiles : [ 'http://example.com/{z}/{x}/{y}.png' ]
413+ } ) ;
414+
415+ const isAbortEvent = ( e : any ) => e ?. abortPendingTileRequests === true || e ?. data ?. abortPendingTileRequests === true ;
416+ let sawAbortEvent = false ;
417+
418+ source . on ( 'data' , ( e : any ) => {
419+ if ( isAbortEvent ( e ) ) sawAbortEvent = true ;
420+ } ) ;
355421
356422 const loadSpy = vi . spyOn ( source , 'load' ) . mockResolvedValue ( undefined as any ) ;
357- loadSpy . mockClear ( ) ;
423+
424+ source . setSourceProperty ( ( ) => { } ) ;
425+
426+ expect ( sawAbortEvent ) . toBe ( true ) ;
427+ expect ( loadSpy ) . toHaveBeenCalledTimes ( 1 ) ;
428+ expect ( loadSpy ) . toHaveBeenCalledWith ( true ) ;
429+ } ) ;
430+
431+ test ( 'setUrl emits abortPendingTileRequests and calls load(true)' , ( ) => {
432+ const source = createSource ( {
433+ tiles : [ 'http://example.com/{z}/{x}/{y}.png' ]
434+ } ) ;
435+
436+ const loadSpy = vi . spyOn ( source , 'load' ) . mockResolvedValue ( undefined as any ) ;
437+ const events : Array < any > = [ ] ;
438+ source . on ( 'data' , ( e : any ) => events . push ( e ) ) ;
358439
359440 source . setUrl ( 'http://localhost:2900/source2.json' ) ;
360441
361- expect ( abortAllRequests ) . toHaveBeenCalledTimes ( 1 ) ;
442+ expect (
443+ events . some ( ( e ) => e ?. abortPendingTileRequests === true || e ?. data ?. abortPendingTileRequests === true )
444+ ) . toBe ( true ) ;
445+ expect ( loadSpy ) . toHaveBeenCalledWith ( true ) ;
446+ } ) ;
447+
448+ test ( 'setUrl emits abortPendingTileRequests and updates url (no TileManager internals)' , ( ) => {
449+ const source = createSource ( {
450+ tiles : [ 'http://example.com/{z}/{x}/{y}.png' ]
451+ } ) ;
452+
453+ const loadSpy = vi . spyOn ( source , 'load' ) . mockResolvedValue ( undefined as any ) ;
454+ const events : Array < any > = [ ] ;
455+ source . on ( 'data' , ( e : any ) => events . push ( e ) ) ;
456+
457+ source . setUrl ( 'http://localhost:2900/source2.json' ) ;
458+
459+ expect (
460+ events . some ( ( e ) => e ?. abortPendingTileRequests === true || e ?. data ?. abortPendingTileRequests === true )
461+ ) . toBe ( true ) ;
362462 expect ( source . url ) . toBe ( 'http://localhost:2900/source2.json' ) ;
463+ expect ( ( source as any ) . _options . url ) . toBe ( 'http://localhost:2900/source2.json' ) ;
363464 expect ( loadSpy ) . toHaveBeenCalledWith ( true ) ;
364465 } ) ;
365466
467+ test ( 'load emits error event on TileJSON network error (non-abort)' , async ( ) => {
468+ const source = createSource ( {
469+ tiles : [ 'http://example.com/{z}/{x}/{y}.png' ]
470+ } ) ;
471+
472+ vi . spyOn ( loadTileJSONModule , 'loadTileJson' ) . mockRejectedValueOnce ( new Error ( 'network failure' ) ) ;
473+
474+ const onError = vi . fn ( ) ;
475+ source . on ( 'error' , onError ) ;
476+
477+ await source . load ( true ) ;
478+
479+ expect ( onError ) . toHaveBeenCalledTimes ( 1 ) ;
480+ } ) ;
481+
482+ test ( 'load ignores AbortError from TileJSON request' , async ( ) => {
483+ const source = createSource ( {
484+ tiles : [ 'http://example.com/{z}/{x}/{y}.png' ]
485+ } ) ;
486+
487+ const abortError = new Error ( 'aborted' ) ;
488+ ( abortError as any ) . name = 'AbortError' ;
489+ vi . spyOn ( loadTileJSONModule , 'loadTileJson' ) . mockRejectedValueOnce ( abortError ) ;
490+
491+ const onError = vi . fn ( ) ;
492+ source . on ( 'error' , onError ) ;
493+
494+ await source . load ( true ) ;
495+
496+ expect ( onError ) . not . toHaveBeenCalled ( ) ;
497+ } ) ;
498+
499+ test ( 'load emits error event when TileJSON is malformed (parser rejection)' , async ( ) => {
500+ const source = createSource ( {
501+ tiles : [ 'http://example.com/{z}/{x}/{y}.png' ]
502+ } ) ;
503+
504+ vi . spyOn ( loadTileJSONModule , 'loadTileJson' ) . mockRejectedValueOnce ( new Error ( 'Invalid TileJSON payload' ) ) ;
505+
506+ const onError = vi . fn ( ) ;
507+ source . on ( 'error' , onError ) ;
508+
509+ await source . load ( true ) ;
510+
511+ expect ( onError ) . toHaveBeenCalledTimes ( 1 ) ;
512+ } ) ;
513+
366514} ) ;
0 commit comments