mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
1761f2bcd5
The default basemap has been a vector style since 4.1.0, and maplibre-gl builds its WebGL context inside the Map constructor. A browser that refuses one throws from there, and VectorBasemap attaches the layer inside an async effect, so the throw became an unhandled rejection no boundary could see. What actually took the page down was the teardown. Leaflet registers a layer before onAdd runs, so the half-built one stayed on the map, and the bridge's onRemove calls this._glMap.remove() without asking whether there is one. That TypeError lands in a React cleanup, which is why the error appears on the way out of a map rather than on the way in, and why it reads as "Something went wrong" when a tab changes and "TREK could not start" when the route does. The browser is now asked first, once per page load, with a one-pixel probe that hands the context straight back. Without WebGL the basemap is OSM raster tiles instead of an empty map, and the maplibre chunk, about a megabyte, is never fetched. The teardown path is safe either way: a half-built GL layer gets a no-op stand-in so the bridge can finish its own cleanup, and detaching never throws, because unmounting a map must not fail just because its basemap did. Reproduced against demo.liketrek.com with WebGL switched off, and verified with the fix: 14 raster tiles drawn, no GL canvas, no error screen. Closes #2288
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
/**
|
|
* The WebGL probe (#2288).
|
|
*
|
|
* What is under test is the contract the basemap depends on: ask the browser
|
|
* once, ask it the way maplibre-gl asks, give the context straight back, and
|
|
* never throw whatever the browser does.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
import { hasWebGL, resetWebGLProbe } from './webgl'
|
|
|
|
const realGetContext = HTMLCanvasElement.prototype.getContext
|
|
|
|
/** A context that records whether its lose-context extension was used. */
|
|
function fakeContext() {
|
|
const loseContext = vi.fn()
|
|
return { ctx: { getExtension: vi.fn(() => ({ loseContext })) }, loseContext }
|
|
}
|
|
|
|
beforeEach(() => {
|
|
resetWebGLProbe()
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
afterEach(() => {
|
|
HTMLCanvasElement.prototype.getContext = realGetContext
|
|
resetWebGLProbe()
|
|
})
|
|
|
|
describe('hasWebGL', () => {
|
|
it('FE-UTIL-WEBGL-001: says yes when the browser hands back a context', () => {
|
|
const { ctx } = fakeContext()
|
|
HTMLCanvasElement.prototype.getContext = vi.fn(() => ctx) as unknown as typeof realGetContext
|
|
|
|
expect(hasWebGL()).toBe(true)
|
|
})
|
|
|
|
it('FE-UTIL-WEBGL-002: says no when it hands back null, which is WebGL switched off', () => {
|
|
// jsdom answers this way too, so the whole suite would take the raster path
|
|
// if this were the other way round.
|
|
HTMLCanvasElement.prototype.getContext = vi.fn(() => null) as unknown as typeof realGetContext
|
|
|
|
expect(hasWebGL()).toBe(false)
|
|
})
|
|
|
|
it('FE-UTIL-WEBGL-003: falls back to webgl1 when only webgl2 is refused', () => {
|
|
// Exactly the order maplibre-gl asks in. A stricter probe would black out
|
|
// maps that would in fact have drawn.
|
|
const { ctx } = fakeContext()
|
|
const getContext = vi.fn((type: string) => (type === 'webgl2' ? null : ctx))
|
|
HTMLCanvasElement.prototype.getContext = getContext as unknown as typeof realGetContext
|
|
|
|
expect(hasWebGL()).toBe(true)
|
|
expect(getContext.mock.calls.map((c) => c[0])).toEqual(['webgl2', 'webgl'])
|
|
})
|
|
|
|
it('FE-UTIL-WEBGL-004: gives the probe context straight back', () => {
|
|
// Contexts are a capped resource, around sixteen in Chrome. A probe that
|
|
// keeps one is a context the map behind it may not get.
|
|
const { ctx, loseContext } = fakeContext()
|
|
HTMLCanvasElement.prototype.getContext = vi.fn(() => ctx) as unknown as typeof realGetContext
|
|
|
|
hasWebGL()
|
|
|
|
expect(ctx.getExtension).toHaveBeenCalledWith('WEBGL_lose_context')
|
|
expect(loseContext).toHaveBeenCalled()
|
|
})
|
|
|
|
it('FE-UTIL-WEBGL-005: asks once and remembers, however often it is called', () => {
|
|
const { ctx } = fakeContext()
|
|
const getContext = vi.fn(() => ctx)
|
|
HTMLCanvasElement.prototype.getContext = getContext as unknown as typeof realGetContext
|
|
|
|
hasWebGL()
|
|
hasWebGL()
|
|
hasWebGL()
|
|
|
|
// One probe, one context. A planner holding a map and a settings preview
|
|
// would otherwise spend three.
|
|
expect(getContext).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('FE-UTIL-WEBGL-006: a browser that throws out of getContext counts as no', () => {
|
|
// Some locked-down builds throw rather than returning null, and an answer is
|
|
// never worth an exception.
|
|
HTMLCanvasElement.prototype.getContext = vi.fn(() => {
|
|
throw new Error('WebGL is disabled by policy')
|
|
}) as unknown as typeof realGetContext
|
|
|
|
expect(hasWebGL()).toBe(false)
|
|
})
|
|
})
|