UNPKG

maplibre-gl

Version:

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

397 lines (337 loc) 15.2 kB
import {describe, beforeEach, afterEach, test, expect, vi} from 'vitest'; import fs from 'fs'; import path from 'path'; import vt from '@mapbox/vector-tile'; import Protobuf from 'pbf'; import {type LoadVectorData, 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} 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'; 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.loading).toEqual({}); await expect(abortPromise).resolves.toBeFalsy(); await expect(loadPromise).rejects.toThrow(ABORT_ERROR); }); test('VectorTileWorkerSource.removeTile removes loaded tile', async () => { const source = new VectorTileWorkerSource(actor, new StyleLayerIndex(), []); source.loaded = { '0': {} as WorkerTile }; const res = await source.removeTile({ source: 'source', uid: 0 } as any as TileParameters); expect(res).toBeUndefined(); expect(source.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.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 loadVectorData: LoadVectorData = async (_params, _abortController) => { 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 [[{x: 0, y: 0}]]; } }) } } } as any as vt.VectorTile, rawData: rawTileData }; }; 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 = loadVectorData; 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()); // 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); 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: LoadVectorData = async (_params, _abortController) => { return { vectorTile: new vt.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); }); }); 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); // 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.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.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, _abortController) => Promise.resolve(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: LoadVectorData = async (_params, _abortController) => { return { vectorTile: new vt.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]); const res = await 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); 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: LoadVectorData = async (_params, _abortController) => { return { vectorTile: new vt.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; }); const res = await 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); expect(res.resourceTiming[0]).toEqual( {'duration': 250, 'entryType': 'measure', 'name': 'http://localhost:2900/faketile.pbf', 'startTime': 100} ); }); });