diff --git a/providers/maplibre/src/index.js b/providers/maplibre/src/index.js index 367ac83b..495ac221 100755 --- a/providers/maplibre/src/index.js +++ b/providers/maplibre/src/index.js @@ -40,7 +40,7 @@ export default function createMapLibreProvider (config = {}) { const mapFramework = await import(/* webpackChunkName: "im-maplibre-framework" */ 'maplibre-gl') if (config.workerUrl) { - mapFramework.workerUrl = config.workerUrl + mapFramework.setWorkerUrl(config.workerUrl) } const MapProvider = (await import(/* webpackChunkName: "im-maplibre-provider" */ './maplibreProvider.js')).default diff --git a/providers/maplibre/src/index.test.js b/providers/maplibre/src/index.test.js index de337715..a54915e1 100644 --- a/providers/maplibre/src/index.test.js +++ b/providers/maplibre/src/index.test.js @@ -1,8 +1,9 @@ import createMapLibreProvider from './index.js' import { getWebGL } from './utils/detectWebgl.js' +import * as maplibreGl from 'maplibre-gl' jest.mock('./utils/detectWebgl.js', () => ({ getWebGL: jest.fn() })) -jest.mock('maplibre-gl', () => ({ VERSION: '3.x' })) +jest.mock('maplibre-gl', () => ({ VERSION: '3.x', setWorkerUrl: jest.fn() })) jest.mock('./maplibreProvider.js', () => ({ default: class MockProvider {} })) describe('createMapLibreProvider', () => { @@ -11,7 +12,7 @@ describe('createMapLibreProvider', () => { }) afterEach(() => { - jest.restoreAllMocks() + jest.clearAllMocks() }) test('checkDeviceCapabilities: WebGL enabled, modern browser, no IE → isSupported true', () => { @@ -75,21 +76,15 @@ describe('createMapLibreProvider', () => { expect(mapProviderConfig).toEqual({ crs: 'EPSG:4326' }) }) - test('load sets workerUrl on mapFramework when provided', async () => { - await jest.isolateModulesAsync(async () => { - const { default: createProvider } = await import('./index.js') - const { mapFramework } = await createProvider({ workerUrl: '/assets/maplibre-gl-csp-worker.js' }).load() + test('load calls setWorkerUrl on mapFramework when workerUrl is provided', async () => { + await createMapLibreProvider({ workerUrl: '/assets/maplibre-gl-csp-worker.js' }).load() - expect(mapFramework.workerUrl).toBe('/assets/maplibre-gl-csp-worker.js') - }) + expect(maplibreGl.setWorkerUrl).toHaveBeenCalledWith('/assets/maplibre-gl-csp-worker.js') }) - test('load does not set workerUrl on mapFramework when not provided', async () => { - await jest.isolateModulesAsync(async () => { - const { default: createProvider } = await import('./index.js') - const { mapFramework } = await createProvider().load() + test('load does not call setWorkerUrl on mapFramework when workerUrl is not provided', async () => { + await createMapLibreProvider().load() - expect(mapFramework.workerUrl).toBeUndefined() - }) + expect(maplibreGl.setWorkerUrl).not.toHaveBeenCalled() }) }) diff --git a/providers/maplibre/src/utils/rasteriseToImageData.js b/providers/maplibre/src/utils/rasteriseToImageData.js index 94776684..20f1a7f9 100644 --- a/providers/maplibre/src/utils/rasteriseToImageData.js +++ b/providers/maplibre/src/utils/rasteriseToImageData.js @@ -10,8 +10,7 @@ const SVG_ERROR_PREVIEW_LENGTH = 80 */ export const rasteriseToImageData = (svgString, width, height) => new Promise((resolve, reject) => { - const blob = new Blob([svgString], { type: 'image/svg+xml' }) - const url = URL.createObjectURL(blob) + const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}` const img = new Image(width, height) img.onload = () => { const canvas = document.createElement('canvas') @@ -19,11 +18,9 @@ export const rasteriseToImageData = (svgString, width, height) => canvas.height = height const ctx = canvas.getContext('2d') ctx.drawImage(img, 0, 0, width, height) - URL.revokeObjectURL(url) resolve(ctx.getImageData(0, 0, width, height)) } img.onerror = () => { - URL.revokeObjectURL(url) reject(new Error(`Failed to rasterise SVG: ${svgString.slice(0, SVG_ERROR_PREVIEW_LENGTH)}`)) } img.src = url diff --git a/providers/maplibre/src/utils/rasteriseToImageData.test.js b/providers/maplibre/src/utils/rasteriseToImageData.test.js index 58f78dd5..3ecb6bb1 100644 --- a/providers/maplibre/src/utils/rasteriseToImageData.test.js +++ b/providers/maplibre/src/utils/rasteriseToImageData.test.js @@ -9,20 +9,25 @@ const ERROR_PREVIEW_LENGTH = 80 // Length chosen to be well over ERROR_PREVIEW_LENGTH so truncation is exercised const LONG_CONTENT_LENGTH = 200 -beforeAll(() => { - globalThis.URL.createObjectURL = jest.fn(() => 'blob:mock') - globalThis.URL.revokeObjectURL = jest.fn() +let imageInstances = [] +beforeAll(() => { HTMLCanvasElement.prototype.getContext = jest.fn(() => ({ drawImage: jest.fn(), getImageData: jest.fn((_x, _y, w, h) => ({ width: w, height: h })) })) +}) + +beforeEach(() => { + imageInstances = [] + jest.clearAllMocks() globalThis.Image = class { constructor (w, h) { this.width = w this.height = h this._src = '' + imageInstances.push(this) } get src () { return this._src } @@ -30,40 +35,34 @@ beforeAll(() => { } }) -beforeEach(() => { - jest.clearAllMocks() - globalThis.URL.createObjectURL.mockReturnValue('blob:mock') -}) - describe('rasteriseToImageData', () => { - it('resolves with ImageData at the requested dimensions, draws via canvas, and revokes the blob URL', async () => { + it('resolves with ImageData at the requested dimensions and draws via canvas', async () => { const getContext = HTMLCanvasElement.prototype.getContext const result = await rasteriseToImageData(SVG, WIDTH, HEIGHT) expect(result).toMatchObject({ width: WIDTH, height: HEIGHT }) - expect(globalThis.URL.createObjectURL).toHaveBeenCalledWith(expect.any(Blob)) - expect(globalThis.URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock') const { drawImage, getImageData } = getContext.mock.results[0].value expect(drawImage).toHaveBeenCalledWith(expect.any(Object), 0, 0, WIDTH, HEIGHT) expect(getImageData).toHaveBeenCalledWith(0, 0, WIDTH, HEIGHT) }) - it('rejects with a truncated SVG preview and revokes the blob URL on error', async () => { - const originalImage = globalThis.Image + it('sets img.src to a data URI', async () => { + await rasteriseToImageData(SVG, WIDTH, HEIGHT) + const src = imageInstances[0]._src + expect(src).toMatch(/^data:image\/svg\+xml;charset=utf-8,/) + expect(src).toContain(encodeURIComponent(SVG)) + }) + + it('rejects with a truncated SVG preview on error', async () => { globalThis.Image = class { constructor (w, h) { this.width = w; this.height = h; this._src = '' } get src () { return this._src } set src (val) { this._src = val; this.onerror?.() } } - try { - const longSvg = `${'x'.repeat(LONG_CONTENT_LENGTH)}` - const error = await rasteriseToImageData(longSvg, WIDTH, HEIGHT).catch(e => e) - expect(error.message).toMatch('Failed to rasterise SVG') - const preview = error.message.replace('Failed to rasterise SVG: ', '') - expect(preview).toHaveLength(ERROR_PREVIEW_LENGTH) - expect(preview).toBe(longSvg.slice(0, ERROR_PREVIEW_LENGTH)) - expect(globalThis.URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock') - } finally { - globalThis.Image = originalImage - } + const longSvg = `${'x'.repeat(LONG_CONTENT_LENGTH)}` + const error = await rasteriseToImageData(longSvg, WIDTH, HEIGHT).catch(e => e) + expect(error.message).toMatch('Failed to rasterise SVG') + const preview = error.message.replace('Failed to rasterise SVG: ', '') + expect(preview).toHaveLength(ERROR_PREVIEW_LENGTH) + expect(preview).toBe(longSvg.slice(0, ERROR_PREVIEW_LENGTH)) }) }) diff --git a/providers/maplibre/src/utils/symbolImages.test.js b/providers/maplibre/src/utils/symbolImages.test.js index f7eeb1f5..b89bc5e7 100644 --- a/providers/maplibre/src/utils/symbolImages.test.js +++ b/providers/maplibre/src/utils/symbolImages.test.js @@ -230,18 +230,18 @@ describe('registerSymbols — null results and caching', () => { const uniqueRatio = 7 const map1 = makeMap() - const blobCallsBefore = globalThis.URL.createObjectURL.mock.calls.length + const getContextCallsBefore = HTMLCanvasElement.prototype.getContext.mock.calls.length await registerSymbols(map1, [{ symbol: 'pin' }], mapStyle, symbolRegistry, uniqueRatio) - const blobCallsAfterFirst = globalThis.URL.createObjectURL.mock.calls.length - // Rasterisation ran — blob was created - expect(blobCallsAfterFirst).toBeGreaterThan(blobCallsBefore) + const getContextCallsAfterFirst = HTMLCanvasElement.prototype.getContext.mock.calls.length + // Rasterisation ran — canvas was used + expect(getContextCallsAfterFirst).toBeGreaterThan(getContextCallsBefore) // Second call with a fresh map (hasImage → false) but same ratio → cache hit const map2 = makeMap() await registerSymbols(map2, [{ symbol: 'pin' }], mapStyle, symbolRegistry, uniqueRatio) - const blobCallsAfterSecond = globalThis.URL.createObjectURL.mock.calls.length - // No new blob created — rasterisation was skipped via cache - expect(blobCallsAfterSecond).toBe(blobCallsAfterFirst) + const getContextCallsAfterSecond = HTMLCanvasElement.prototype.getContext.mock.calls.length + // No new canvas — rasterisation was skipped via cache + expect(getContextCallsAfterSecond).toBe(getContextCallsAfterFirst) // addImage still called because map2 has no pre-registered images expect(map2.addImage).toHaveBeenCalledTimes(2) })