Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion providers/maplibre/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 9 additions & 14 deletions providers/maplibre/src/index.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -11,7 +12,7 @@ describe('createMapLibreProvider', () => {
})

afterEach(() => {
jest.restoreAllMocks()
jest.clearAllMocks()
})

test('checkDeviceCapabilities: WebGL enabled, modern browser, no IE → isSupported true', () => {
Expand Down Expand Up @@ -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()
})
})
5 changes: 1 addition & 4 deletions providers/maplibre/src/utils/rasteriseToImageData.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,17 @@ 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')
canvas.width = width
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
Expand Down
47 changes: 23 additions & 24 deletions providers/maplibre/src/utils/rasteriseToImageData.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,60 @@ 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 }
set src (val) { this._src = val; this.onload?.() }
}
})

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 = `<svg>${'x'.repeat(LONG_CONTENT_LENGTH)}</svg>`
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 = `<svg>${'x'.repeat(LONG_CONTENT_LENGTH)}</svg>`
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))
})
})
14 changes: 7 additions & 7 deletions providers/maplibre/src/utils/symbolImages.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
Loading