@@ -7,6 +7,11 @@ 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' ;
11+
12+ function isAbortPendingTileRequestsEvent ( e : any ) : boolean {
13+ return e ?. abortPendingTileRequests === true || e ?. data ?. abortPendingTileRequests === true ;
14+ }
1015
1116function createSource ( options , transformCallback ?) {
1217 const source = new RasterTileSource ( 'id' , options , { send ( ) { } } as any as Dispatcher , options . eventedParent ) ;
@@ -31,6 +36,7 @@ describe('RasterTileSource', () => {
3136
3237 afterEach ( ( ) => {
3338 server . restore ( ) ;
39+ vi . restoreAllMocks ( ) ;
3440 } ) ;
3541
3642 test ( 'transforms request for TileJSON URL' , ( ) => {
@@ -339,4 +345,118 @@ describe('RasterTileSource', () => {
339345 await expect ( loadPromise ) . resolves . toBeUndefined ( ) ;
340346 expect ( tile . state ) . toBe ( 'unloaded' ) ;
341347 } ) ;
348+
349+ test ( 'loads tile after previous abort flag was set' , async ( ) => {
350+ server . respondWith ( '/source.json' , JSON . stringify ( {
351+ minzoom : 0 ,
352+ maxzoom : 22 ,
353+ attribution : 'MapLibre' ,
354+ tiles : [ 'http://example.com/{z}/{x}/{y}.png' ] ,
355+ bounds : [ - 47 , - 7 , - 45 , - 5 ]
356+ } ) ) ;
357+ server . respondWith ( 'http://example.com/10/5/5.png' ,
358+ [ 200 , { 'Content-Type' : 'image/png' , 'Content-Length' : 1 } , '0' ]
359+ ) ;
360+
361+ const source = createSource ( { url : '/source.json' } ) ;
362+ source . map . painter = { context : { } , getTileTexture : ( ) => ( { update : ( ) => { } } ) } as any ;
363+
364+ const sourcePromise = waitForEvent ( source , 'data' , ( e : MapSourceDataEvent ) => e . sourceDataType === 'metadata' ) ;
365+ server . respond ( ) ;
366+ await sourcePromise ;
367+
368+ const tile = {
369+ tileID : new OverscaledTileID ( 10 , 0 , 10 , 5 , 5 ) ,
370+ state : 'loading' ,
371+ aborted : true ,
372+ setExpiryData ( ) { }
373+ } as any as Tile ;
374+
375+ expect ( tile . aborted ) . toBe ( true ) ;
376+
377+ const tilePromise = source . loadTile ( tile ) ;
378+ server . respond ( ) ;
379+ await tilePromise ;
380+
381+ expect ( tile . state ) . toBe ( 'loaded' ) ;
382+ expect ( tile . aborted ) . toBe ( false ) ;
383+ } ) ;
384+
385+ test ( 'setSourceProperty emits abortPendingTileRequests before callback and load(true)' , ( ) => {
386+ const source = createSource ( {
387+ tiles : [ 'http://example.com/{z}/{x}/{y}.png' ]
388+ } ) ;
389+ const onData = vi . fn ( ) ;
390+ source . on ( 'data' , onData ) ;
391+
392+ const callback = vi . fn ( ) ;
393+ const loadSpy = vi . spyOn ( source , 'load' ) ;
394+
395+ source . setSourceProperty ( callback ) ;
396+
397+ const abortEventIndex = onData . mock . calls . findIndex ( ( [ e ] ) => isAbortPendingTileRequestsEvent ( e ) ) ;
398+ expect ( abortEventIndex ) . toBeGreaterThan ( - 1 ) ;
399+
400+ const abortEventOrder = onData . mock . invocationCallOrder [ abortEventIndex ] ;
401+ const callbackOrder = callback . mock . invocationCallOrder [ 0 ] ;
402+ const loadOrder = loadSpy . mock . invocationCallOrder [ 0 ] ;
403+
404+ expect ( abortEventOrder ) . toBeLessThan ( callbackOrder ) ;
405+ expect ( callbackOrder ) . toBeLessThan ( loadOrder ) ;
406+
407+ expect ( callback ) . toHaveBeenCalledTimes ( 1 ) ;
408+ expect ( loadSpy ) . toHaveBeenCalledTimes ( 1 ) ;
409+ expect ( loadSpy ) . toHaveBeenCalledWith ( true ) ;
410+ } ) ;
411+
412+ test ( 'setUrl emits abortPendingTileRequests and calls load(true)' , ( ) => {
413+ const source = createSource ( {
414+ tiles : [ 'http://example.com/{z}/{x}/{y}.png' ]
415+ } ) ;
416+
417+ const loadSpy = vi . spyOn ( source , 'load' ) ;
418+ const events : Array < any > = [ ] ;
419+ source . on ( 'data' , ( e : any ) => events . push ( e ) ) ;
420+
421+ source . setUrl ( 'http://localhost:2900/source2.json' ) ;
422+
423+ expect (
424+ events . some ( ( e ) => isAbortPendingTileRequestsEvent ( e ) )
425+ ) . toBe ( true ) ;
426+ expect ( source . url ) . toBe ( 'http://localhost:2900/source2.json' ) ;
427+ expect ( loadSpy ) . toHaveBeenCalledWith ( true ) ;
428+ } ) ;
429+
430+ test ( 'load ignores AbortError from TileJSON request' , async ( ) => {
431+ const source = createSource ( {
432+ tiles : [ 'http://example.com/{z}/{x}/{y}.png' ]
433+ } ) ;
434+
435+ const abortError = new Error ( 'aborted' ) ;
436+ ( abortError as any ) . name = 'AbortError' ;
437+ vi . spyOn ( loadTileJSONModule , 'loadTileJson' ) . mockRejectedValueOnce ( abortError ) ;
438+
439+ const onError = vi . fn ( ) ;
440+ source . on ( 'error' , onError ) ;
441+
442+ await source . load ( true ) ;
443+
444+ expect ( onError ) . not . toHaveBeenCalled ( ) ;
445+ } ) ;
446+
447+ test ( 'load emits error event when TileJSON is malformed (parser rejection)' , async ( ) => {
448+ const source = createSource ( {
449+ tiles : [ 'http://example.com/{z}/{x}/{y}.png' ]
450+ } ) ;
451+
452+ vi . spyOn ( loadTileJSONModule , 'loadTileJson' ) . mockRejectedValueOnce ( new Error ( 'Invalid TileJSON payload' ) ) ;
453+
454+ const onError = vi . fn ( ) ;
455+ source . on ( 'error' , onError ) ;
456+
457+ await source . load ( true ) ;
458+
459+ expect ( onError ) . toHaveBeenCalledTimes ( 1 ) ;
460+ } ) ;
461+
342462} ) ;
0 commit comments