UNPKG

maplibre-gl

Version:

BSD licensed community fork of mapbox-gl, a WebGL interactive maps library

493 lines (411 loc) 18.4 kB
import {describe, beforeEach, afterEach, test, expect, vi} from 'vitest'; import fs from 'fs'; import path from 'path'; import Protobuf from 'pbf'; import {VectorTileWorkerSource} from '../source/vector_tile_worker_source'; import {StyleLayerIndex} from '../style/style_layer_index'; import {fakeServer, type FakeServer} from 'nise'; import {type IActor} from '../util/actor'; import {type TileParameters, type WorkerTileParameters, type WorkerTileResult, type WorkerTileWithData} from './worker_source'; import {WorkerTile} from './worker_tile'; import {setPerformance, sleep} from '../util/test/util'; import {ABORT_ERROR} from '../util/abort_error'; import {SubdivisionGranularitySetting} from '../render/subdivision_granularity_settings'; import {OverscaledTileID, CanonicalTileID} from '../tile/tile_id'; import {VectorTile} from '@mapbox/vector-tile'; import Point from '@mapbox/point-geometry'; describe('vector tile worker source', () => { const actor = {sendAsync: () => Promise.resolve({})} as IActor; let server: FakeServer; beforeEach(() => { global.fetch = null; server = fakeServer.create(); setPerformance(); }); afterEach(() => { server.restore(); vi.clearAllMocks(); }); test('VectorTileWorkerSource.abortTile aborts pending request', async () => { const source = new VectorTileWorkerSource(actor, new StyleLayerIndex(), []); const loadPromise = source.loadTile({ source: 'source', uid: 0, tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, request: {url: 'http://localhost:2900/abort'} } as any as WorkerTileParameters); const abortPromise = source.abortTile({ source: 'source', uid: 0 } as any as TileParameters); expect(source.tileState.loading).toEqual({}); await expect(abortPromise).resolves.toBeFalsy(); await expect(loadPromise).rejects.toThrow(expect.objectContaining({name: ABORT_ERROR})); }); test('VectorTileWorkerSource.removeTile removes loaded tile', async () => { const source = new VectorTileWorkerSource(actor, new StyleLayerIndex(), []); source.tileState.loaded = { '0': {} as WorkerTile }; const res = await source.removeTile({ source: 'source', uid: 0 } as any as TileParameters); expect(res).toBeUndefined(); expect(source.tileState.loaded).toEqual({}); }); test('VectorTileWorkerSource.reloadTile reloads a previously-loaded tile', async () => { const source = new VectorTileWorkerSource(actor, new StyleLayerIndex(), []); const parse = vi.fn().mockReturnValue(Promise.resolve({} as WorkerTileResult)); source.tileState.loaded = { '0': { status: 'done', vectorTile: {}, parse } as any as WorkerTile }; const reloadPromise = source.reloadTile({uid: 0} as any as WorkerTileParameters); expect(parse).toHaveBeenCalledTimes(1); await expect(reloadPromise).resolves.toBeTruthy(); }); test('VectorTileWorkerSource.loadTile reparses tile if the reloadTile has been called during parsing', async () => { const rawTileData = new ArrayBuffer(0); const layerIndex = new StyleLayerIndex([{ id: 'test', source: 'source', 'source-layer': 'test', type: 'symbol', layout: { 'icon-image': 'hello', 'text-font': ['StandardFont-Bold'], 'text-field': '{name}' } }]); const actor = { sendAsync: (message: {type: string; data: unknown}, abortController: AbortController) => { return new Promise((resolve, _reject) => { const res = setTimeout(() => { const response = message.type === 'getImages' ? {'hello': {width: 1, height: 1, data: new Uint8Array([0])}} : {'StandardFont-Bold': {width: 1, height: 1, data: new Uint8Array([0])}}; resolve(response); }, 100); abortController.signal.addEventListener('abort', () => { clearTimeout(res); }); }); } }; const source = new VectorTileWorkerSource(actor, layerIndex, ['hello']); source.loadVectorTile = (_params, _rawData) => { return { vectorTile: { layers: { test: { version: 2, name: 'test', extent: 8192, length: 1, feature: (featureIndex: number) => ({ extent: 8192, type: 1, id: featureIndex, properties: { name: 'test' }, loadGeometry () { return [[new Point(0, 0)]]; } }) } } }, rawData: rawTileData }; }; server.respondWith(request => { request.respond(200, {'Content-Type': 'application/pbf'}, rawTileData as any); }); source.loadTile({ source: 'source', uid: 0, tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, request: {url: 'http://localhost:2900/faketile.pbf'}, subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision, } as any as WorkerTileParameters).then(() => expect(false).toBeTruthy()); server.respond(); // allow promise to run await sleep(0); const res = await source.reloadTile({ source: 'source', uid: 0, tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision, } as any as WorkerTileParameters) as WorkerTileWithData; expect(res).toBeDefined(); expect(res.rawTileData).toBeDefined(); expect(res.rawTileData).toStrictEqual(rawTileData); }); test('VectorTileWorkerSource.loadTile reparses tile if reloadTile is called during reparsing', async () => { const rawTileData = new ArrayBuffer(0); const loadVectorData = (_params, _rawData) => { return { vectorTile: new VectorTile(new Protobuf(rawTileData)), rawData: rawTileData }; }; const layerIndex = new StyleLayerIndex([{ id: 'test', source: 'source', 'source-layer': 'test', type: 'fill' }]); const source = new VectorTileWorkerSource(actor, layerIndex, []); source.loadVectorTile = loadVectorData; const parseWorkerTileMock = vi .spyOn(WorkerTile.prototype, 'parse') .mockImplementation(function(_data, _layerIndex, _availableImages, _actor) { this.status = 'parsing'; return new Promise((resolve) => { setTimeout(() => resolve({} as WorkerTileResult), 20); }); }); server.respondWith(request => { request.respond(200, {'Content-Type': 'application/pbf'}, rawTileData as any); }); const loadPromise = source.loadTile({ source: 'source', uid: 0, tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, request: {url: 'http://localhost:2900/faketile.pbf'} } as any as WorkerTileParameters); server.respond(); // let the promise start await sleep(0); const res = await source.reloadTile({ source: 'source', uid: '0', tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, } as any as WorkerTileParameters); expect(res).toBeDefined(); expect(parseWorkerTileMock).toHaveBeenCalledTimes(2); await expect(loadPromise).resolves.toBeTruthy(); }); test('VectorTileWorkerSource loadTile uses _getOverzoomTile when overzoomParameters is provided', async () => { const source = new VectorTileWorkerSource({} as any, new StyleLayerIndex(), []); const mockVectorTile = {layers: {}} as any; source.loadVectorTile = vi.fn().mockReturnValue({ vectorTile: mockVectorTile, rawData: new ArrayBuffer(0) }); const getOverzoomTileSpy = vi.spyOn(source as any, '_getOverzoomTile').mockReturnValue({ vectorTile: mockVectorTile, rawData: new ArrayBuffer(0) }); server.respondWith(request => { request.respond(200, {'Content-Type': 'application/pbf'}, new ArrayBuffer(0) as any); }); const params = { uid: '1', tileID: new OverscaledTileID(16, 0, 16, 100, 100), source: 'test', overzoomParameters: { maxZoomTileID: new CanonicalTileID(14, 25, 25), overzoomRequest: {url: ''} } } as WorkerTileParameters; const promise = source.loadTile(params); server.respond(); await promise; expect(getOverzoomTileSpy).toHaveBeenCalledWith(params, mockVectorTile); }); test('VectorTileWorkerSource.reloadTile does not reparse tiles with no vectorTile data but does call callback', async () => { const source = new VectorTileWorkerSource(actor, new StyleLayerIndex(), []); const parse = vi.fn(); source.tileState.loaded = { '0': { status: 'done', parse } as any as WorkerTile }; await source.reloadTile({uid: 0} as any as WorkerTileParameters); expect(parse).not.toHaveBeenCalled(); }); test('VectorTileWorkerSource.loadTile returns null for an empty tile', async () => { const source = new VectorTileWorkerSource(actor, new StyleLayerIndex(), []); source.loadVectorTile = (_params, _rawData) => null; const parse = vi.fn(); server.respondWith(request => { request.respond(200, {'Content-Type': 'application/pbf'}, 'something...'); }); const promise = source.loadTile({ source: 'source', uid: 0, tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, request: {url: 'http://localhost:2900/faketile.pbf'} } as any as WorkerTileParameters); server.respond(); expect(parse).not.toHaveBeenCalled(); expect(await promise).toBeNull(); }); test('VectorTileWorkerSource.returns a good error message when failing to parse a tile', async () => { const source = new VectorTileWorkerSource(actor, new StyleLayerIndex(), []); const parse = vi.fn(); server.respondWith(request => { request.respond(200, {'Content-Type': 'application/pbf'}, 'something...'); }); const loadTilePromise = source.loadTile({ source: 'source', uid: 0, tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, request: {url: 'http://localhost:2900/faketile.pbf'} } as any as WorkerTileParameters); server.respond(); expect(parse).not.toHaveBeenCalled(); await expect(loadTilePromise).rejects.toThrowError(/Unable to parse the tile at/); }); test('VectorTileWorkerSource.returns a good error message when failing to parse a gzipped tile', async () => { const source = new VectorTileWorkerSource(actor, new StyleLayerIndex(), []); const parse = vi.fn(); server.respondWith(new Uint8Array([0x1f, 0x8b]).buffer); const loadTilePromise = source.loadTile({ source: 'source', uid: 0, tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, request: {url: 'http://localhost:2900/faketile.pbf'} } as any as WorkerTileParameters); server.respond(); expect(parse).not.toHaveBeenCalled(); await expect(loadTilePromise).rejects.toThrowError(/gzipped/); }); test('VectorTileWorkerSource provides resource timing information', async () => { const rawTileData = fs.readFileSync(path.join(__dirname, '/../../test/unit/assets/mbsv5-6-18-23.vector.pbf')).buffer.slice(0) as ArrayBuffer; const loadVectorData = (_params, _rawData) => { return { vectorTile: new VectorTile(new Protobuf(rawTileData)), rawData: rawTileData, cacheControl: null, expires: null }; }; const exampleResourceTiming = { connectEnd: 473, connectStart: 473, decodedBodySize: 86494, domainLookupEnd: 473, domainLookupStart: 473, duration: 341, encodedBodySize: 52528, entryType: 'resource', fetchStart: 473.5, initiatorType: 'xmlhttprequest', name: 'http://localhost:2900/faketile.pbf', nextHopProtocol: 'http/1.1', redirectEnd: 0, redirectStart: 0, requestStart: 477, responseEnd: 815, responseStart: 672, secureConnectionStart: 0 }; const layerIndex = new StyleLayerIndex([{ id: 'test', source: 'source', 'source-layer': 'test', type: 'fill' }]); const source = new VectorTileWorkerSource(actor, layerIndex, []); source.loadVectorTile = loadVectorData; window.performance.getEntriesByName = vi.fn().mockReturnValue([exampleResourceTiming]); server.respondWith(request => { request.respond(200, {'Content-Type': 'application/pbf'}, 'ok'); }); const promise = source.loadTile({ source: 'source', uid: 0, tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, request: {url: 'http://localhost:2900/faketile.pbf', collectResourceTiming: true} } as any as WorkerTileParameters); await sleep(0); server.respond(); const res = await promise; expect(res.resourceTiming[0]).toEqual(exampleResourceTiming); }); test('VectorTileWorkerSource provides resource timing information (fallback method)', async () => { const rawTileData = fs.readFileSync(path.join(__dirname, '/../../test/unit/assets/mbsv5-6-18-23.vector.pbf')).buffer.slice(0) as ArrayBuffer; const loadVectorData = (_params, _rawData) => { return { vectorTile: new VectorTile(new Protobuf(rawTileData)), rawData: rawTileData, cacheControl: null, expires: null }; }; const layerIndex = new StyleLayerIndex([{ id: 'test', source: 'source', 'source-layer': 'test', type: 'fill' }]); const source = new VectorTileWorkerSource(actor, layerIndex, []); source.loadVectorTile = loadVectorData; const sampleMarks = [100, 350]; const marks = {}; const measures = {}; window.performance.getEntriesByName = vi.fn().mockImplementation(name => (measures[name] || [])); window.performance.mark = vi.fn().mockImplementation(name => { marks[name] = sampleMarks.shift(); return null; }); window.performance.measure = vi.fn().mockImplementation((name, start, end) => { measures[name] = measures[name] || []; measures[name].push({ duration: marks[end] - marks[start], entryType: 'measure', name, startTime: marks[start] }); return null; }); server.respondWith(request => { request.respond(200, {'Content-Type': 'application/pbf'}, 'ok'); }); const promise = source.loadTile({ source: 'source', uid: 0, tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, request: {url: 'http://localhost:2900/faketile.pbf', collectResourceTiming: true} } as any as WorkerTileParameters); await sleep(0); server.respond(); const res = await promise; expect(res.resourceTiming[0]).toEqual( {'duration': 250, 'entryType': 'measure', 'name': 'http://localhost:2900/faketile.pbf', 'startTime': 100} ); }); test('VectorTileWorkerSource.loadTile skips parsing and returns unmodified when e-tags match', async () => { const source = new VectorTileWorkerSource(actor, new StyleLayerIndex(), []); source.loadVectorTile = () => { throw new Error('loadVectorTile should not be called when etag matches'); }; const rawTileData = new ArrayBuffer(0); server.respondWith(request => { request.respond(200, { 'Content-Type': 'application/pbf', 'ETag': '"v1"' }, rawTileData as any); }); const promise = source.loadTile({ source: 'source', uid: 0, tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, request: {url: 'http://localhost:2900/faketile.pbf'}, etag: '"v1"', subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision, } as any as WorkerTileParameters); server.respond(); const res = await promise; expect(res).toBeDefined(); expect(res.etagUnmodified).toBe(true); }); });