diff --git a/client/src/components/Map/engines/rtlRegistration.test.ts b/client/src/components/Map/engines/rtlRegistration.test.ts new file mode 100644 index 000000000..b2cfb1585 --- /dev/null +++ b/client/src/components/Map/engines/rtlRegistration.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +/** + * The two lines this fix actually adds. Everything else is a helper: the guard in + * rtlText.ts has its own tests, but a guard nobody calls looks exactly like a + * working fix from the outside. Deleting either registration, swapping an + * argument or dropping the deferred flag left the whole suite green. + * + * The engines are mocked because importing them for real builds a worker pool + * under jsdom, and the plugin URL is mocked so the 133 KB payload never loads. + */ +const { mapboxSet, maplibreSet } = vi.hoisted(() => ({ + mapboxSet: vi.fn(), + maplibreSet: vi.fn(), +})); + +vi.mock('mapbox-gl', () => ({ default: { setRTLTextPlugin: mapboxSet } })); +vi.mock('maplibre-gl', () => ({ default: { setRTLTextPlugin: maplibreSet } })); +vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({})); +vi.mock('maplibre-gl/dist/maplibre-gl.css', () => ({})); +vi.mock('@mapbox/mapbox-gl-rtl-text/dist/mapbox-gl-rtl-text.js?url', () => ({ default: '/assets/rtl-plugin.js' })); + +beforeEach(() => { + vi.resetModules(); + mapboxSet.mockReset(); + maplibreSet.mockReset(); +}); + +describe('the GL engines register the RTL text plugin (#2235)', () => { + it('FE-MAP-RTL-001: importing the mapbox engine registers it deferred, with a null callback', async () => { + await import('./mapbox'); + + expect(mapboxSet).toHaveBeenCalledTimes(1); + // Mapbox GL 3 takes (url, callback, deferred). The third argument is what + // keeps the payload off the wire until a map paints RTL text; dropping it + // would fetch 133 KB for every user on every map. + expect(mapboxSet).toHaveBeenCalledWith(expect.stringContaining('rtl'), null, true); + }); + + it('FE-MAP-RTL-002: importing the maplibre engine registers it lazily', async () => { + await import('./maplibre'); + + expect(maplibreSet).toHaveBeenCalledTimes(1); + // MapLibre 5 takes (url, lazy) and returns a promise, which is the reason + // the call stays with each engine instead of going through one wrapper. + expect(maplibreSet).toHaveBeenCalledWith(expect.stringContaining('rtl'), true); + }); + + it('FE-MAP-RTL-003: an engine still loads when its registration throws', async () => { + mapboxSet.mockImplementation(() => { throw new Error('already registered'); }); + + const engine = await import('./mapbox'); + + expect(engine.default).toBeDefined(); + }); + + it('FE-MAP-RTL-004: a rejected registration promise does not surface as an unhandled rejection', async () => { + maplibreSet.mockReturnValue(Promise.reject(new Error('already registered'))); + + const engine = await import('./maplibre'); + + expect(engine.default).toBeDefined(); + // Give the rejection a tick to go unhandled if nothing caught it. + await new Promise((resolve) => setTimeout(resolve, 0)); + }); +}); diff --git a/client/vite.config.js b/client/vite.config.js index 30e28be1c..6ca0171a1 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -47,8 +47,14 @@ export default defineConfig(({ mode }) => ({ // Every route chunk is precached alongside the shell, deliberately: for an // offline-first travel planner a route the user never opened before losing // signal still has to work. The trade is that splitting buys first paint and - // not install size — 107 entries / 17,795 KiB before any of it, 220 / - // 17,855 KiB now. + // not install size: 107 entries / 17,795 KiB before any of it, 463 / + // 23,292 KiB now (measured, not estimated). + // + // Keep this figure honest. #2228 traced PWA boot failures to the browser + // evicting this origin's whole bucket, precached shell included, and this + // comment is the only record of what the install actually costs. Anything + // matching the globs below is fetched at service-worker install by every + // user, whether or not they ever reach the code. globPatterns: ['**/*.{js,css,html,svg,png,woff,woff2,ttf}'], // build:analyze drops a treemap next to the app; it must never end up in a // precache manifest if someone ships that build by accident.