mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
c089e75f08
* perf(map): load one GL engine instead of both MapViewGL, JourneyMapGL and the settings preview each imported mapbox-gl and maplibre-gl statically, so rollup emitted one 2.83 MB chunk carrying both. Every map user downloaded 1.8 MB of mapbox plus 1.03 MB of maplibre and then ran exactly one of them. The 110 kB stylesheet was merged the same way. mapbox users 2,827,647 -> 1,797,952 B (-1.03 MB, CSS 110.6 -> 40.8 kB) maplibre users 2,827,647 -> 1,027,635 B (-1.80 MB, CSS 110.6 -> 69.8 kB) The engine is a prop now rather than a module import, and the pairing happens behind the lazy boundary in glLazy.tsx. No module reaches both SDKs statically any more, which is the whole mechanism — the components themselves stayed engine-agnostic, they already guarded on the provider everywhere the two genuinely differ. Both imports of a pair start together instead of in sequence: the engine is by far the larger half, and fetching the component first would put its round trip in front of the download that actually costs time. check:gl-split guards it. A stray static import anywhere in the map tree brings the merged chunk straight back with a green build and a working app, so the script greps the built chunks for a marker of each SDK and fails when one file holds both. Not on the bare string "maplibre" — the provider value and the maplibre_style setting key appear in app code too, so that matches the shared core chunk as well. Two boundaries were missing and are added here, because the split makes them reachable: the GL boundaries had no resetKeys, so a chunk failure under one provider kept showing Leaflet after switching to the other, and the settings preview had a Suspense but no boundary at all — that is the one place a user flips providers live, so it is the likeliest chunk to fail. Worth being precise about the gain: the service worker precaches every chunk, so an installed PWA still downloads both engines. What this buys is the critical path on a cold cache, and parse/compile/execute always — a megabyte of unused engine ran on every map open. * perf(client): split out the video player and the ISO subdivision data Two heavyweights that shipped with routes that rarely use them. plyr (112 kB raw / 33 kB gzip, plus 32 kB of CSS) hung off three lightboxes and through them off the FileManager, JournalBody and MTripShell chunks. The lightboxes only render the player on their video branch anyway, so the split costs one frame of placeholder — a black rectangle in the target geometry, so the lightbox does not jump when the chunk lands. iso-3166-2 is a single 238 kB data blob with no tree-shaking to be had, and only fetchRegionOptions touches it. It now loads after the hasRegions check, so it is fetched only for countries that get a region picker at all: holidayRegions drops from 257 kB to 14 kB. The import sits inside the existing try — a failed chunk yields the same empty array as a failed request, rather than an unhandled rejection in the two callers that only do .then(setRegions). tz-lookup is deliberately left alone. TransitSearchPanel builds the departure time it sends to MOTIS out of tzAt(), so a not-yet-loaded window there is a wrong query rather than a late pixel — that one needs its own change with its own care.
220 lines
8.6 KiB
TypeScript
220 lines
8.6 KiB
TypeScript
// FE-COMP-GLPREVIEW-001 to FE-COMP-GLPREVIEW-014
|
|
import { render, screen, act } from '../../../tests/helpers/render';
|
|
|
|
type ClickHandler = (e: { lngLat: { lat: number; lng: number } }) => void;
|
|
|
|
const glHandlers = vi.hoisted(() => ({
|
|
load: undefined as (() => void) | undefined,
|
|
click: undefined as ClickHandler | undefined,
|
|
reset() {
|
|
this.load = undefined;
|
|
this.click = undefined;
|
|
},
|
|
}));
|
|
|
|
const glMap = vi.hoisted(() => ({
|
|
on: vi.fn((event: string, cb: unknown) => {
|
|
if (event === 'load') glHandlers.load = cb as () => void;
|
|
if (event === 'click') glHandlers.click = cb as ClickHandler;
|
|
}),
|
|
remove: vi.fn(),
|
|
jumpTo: vi.fn(),
|
|
setTerrain: vi.fn(),
|
|
}));
|
|
|
|
const mapboxCtor = vi.hoisted(() => vi.fn(function (_options: Record<string, unknown>) {
|
|
return glMap;
|
|
}));
|
|
const maplibreCtor = vi.hoisted(() => vi.fn(function (_options: Record<string, unknown>) {
|
|
return glMap;
|
|
}));
|
|
const mapboxModule = vi.hoisted(() => ({ accessToken: '', Map: mapboxCtor }));
|
|
|
|
vi.mock('mapbox-gl', () => ({ default: mapboxModule }));
|
|
vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({}));
|
|
vi.mock('maplibre-gl', () => ({ default: { Map: maplibreCtor } }));
|
|
vi.mock('maplibre-gl/dist/maplibre-gl.css', () => ({}));
|
|
|
|
vi.mock('../Map/mapboxSetup', () => ({
|
|
isStandardFamily: vi.fn(() => false),
|
|
supportsCustom3d: vi.fn(() => true),
|
|
addCustom3dBuildings: vi.fn(),
|
|
addTerrainAndSky: vi.fn(),
|
|
}));
|
|
|
|
import { isStandardFamily, supportsCustom3d, addCustom3dBuildings, addTerrainAndSky } from '../Map/mapboxSetup';
|
|
import { MAPBOX_DEFAULT_STYLE, OPENFREEMAP_DEFAULT_STYLE } from '../Map/glProviders';
|
|
import mapboxgl from 'mapbox-gl';
|
|
import maplibregl from 'maplibre-gl';
|
|
import GlMapPreviewWithEngine from './MapboxPreview';
|
|
|
|
// The engine is a prop now, not a module import — that is what keeps mapbox-gl and
|
|
// maplibre-gl in separate chunks. This shim preserves the suite's existing call
|
|
// shape and makes the same choice glLazy.tsx makes in production, so the vi.mock
|
|
// factories below still stand in for the right SDK.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function GlMapPreview(props: any) {
|
|
return <GlMapPreviewWithEngine {...props} gl={props.provider === 'maplibre-gl' ? maplibregl : mapboxgl} />;
|
|
}
|
|
|
|
const STREETS = 'mapbox://styles/mapbox/streets-v12';
|
|
|
|
function mapOptions(ctor: typeof mapboxCtor): Record<string, unknown> {
|
|
return ctor.mock.calls[0][0] as Record<string, unknown>;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
glHandlers.reset();
|
|
mapboxModule.accessToken = '';
|
|
document.documentElement.classList.remove('dark');
|
|
vi.mocked(isStandardFamily).mockReturnValue(false);
|
|
vi.mocked(supportsCustom3d).mockReturnValue(true);
|
|
});
|
|
|
|
describe('GlMapPreview', () => {
|
|
it('FE-COMP-GLPREVIEW-001: without a Mapbox token it shows the hint instead of a map', () => {
|
|
render(<GlMapPreview token="" style={STREETS} lat={48.8} lng={2.3} zoom={16} enable3d={false} />);
|
|
|
|
expect(screen.getByText('Enter a Mapbox access token to preview')).toBeInTheDocument();
|
|
expect(mapboxCtor).not.toHaveBeenCalled();
|
|
expect(glMap.jumpTo).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-COMP-GLPREVIEW-002: a token creates a Mapbox map centred on the given coordinates', () => {
|
|
render(<GlMapPreview token="pk.test" style={STREETS} lat={48.8566} lng={2.3522} zoom={16} enable3d={false} />);
|
|
|
|
expect(mapboxModule.accessToken).toBe('pk.test');
|
|
expect(mapboxCtor).toHaveBeenCalledTimes(1);
|
|
expect(mapOptions(mapboxCtor)).toMatchObject({
|
|
style: STREETS,
|
|
center: [2.3522, 48.8566],
|
|
zoom: 16,
|
|
pitch: 0,
|
|
antialias: false,
|
|
projection: 'mercator',
|
|
});
|
|
expect(screen.queryByText('Enter a Mapbox access token to preview')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-GLPREVIEW-003: 3D pitches the camera on mount', () => {
|
|
render(<GlMapPreview token="pk.test" style={STREETS} lat={48.8} lng={2.3} zoom={16} enable3d />);
|
|
|
|
expect(mapOptions(mapboxCtor).pitch).toBe(45);
|
|
});
|
|
|
|
it('FE-COMP-GLPREVIEW-004: quality mode enables the globe projection and antialiasing', () => {
|
|
render(<GlMapPreview token="pk.test" style={STREETS} lat={48.8} lng={2.3} zoom={16} enable3d={false} quality />);
|
|
|
|
expect(mapOptions(mapboxCtor)).toMatchObject({ projection: 'globe', antialias: true });
|
|
});
|
|
|
|
it('FE-COMP-GLPREVIEW-005: MapLibre needs no token and never asks for a projection', () => {
|
|
render(
|
|
<GlMapPreview
|
|
provider="maplibre-gl"
|
|
style={OPENFREEMAP_DEFAULT_STYLE}
|
|
lat={48.8}
|
|
lng={2.3}
|
|
zoom={16}
|
|
enable3d
|
|
/>,
|
|
);
|
|
|
|
expect(maplibreCtor).toHaveBeenCalledTimes(1);
|
|
expect(mapboxCtor).not.toHaveBeenCalled();
|
|
const opts = mapOptions(maplibreCtor);
|
|
expect(opts).toMatchObject({ aroundCenter: false, style: OPENFREEMAP_DEFAULT_STYLE, pitch: 0 });
|
|
expect(opts).not.toHaveProperty('projection');
|
|
});
|
|
|
|
it('FE-COMP-GLPREVIEW-006: a Mapbox style is coerced to the OpenFreeMap default under MapLibre', () => {
|
|
render(<GlMapPreview provider="maplibre-gl" style={STREETS} lat={48.8} lng={2.3} zoom={16} enable3d={false} />);
|
|
|
|
expect(mapOptions(maplibreCtor).style).toBe(OPENFREEMAP_DEFAULT_STYLE);
|
|
});
|
|
|
|
it('FE-COMP-GLPREVIEW-007: loading a 3D-capable custom style adds terrain and buildings', () => {
|
|
render(<GlMapPreview token="pk.test" style={STREETS} lat={48.8} lng={2.3} zoom={16} enable3d />);
|
|
act(() => glHandlers.load?.());
|
|
|
|
expect(addTerrainAndSky).toHaveBeenCalledWith(glMap);
|
|
expect(addCustom3dBuildings).toHaveBeenCalledWith(glMap, false);
|
|
expect(glMap.setTerrain).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-COMP-GLPREVIEW-008: dark mode is passed through to the 3D building colours', () => {
|
|
document.documentElement.classList.add('dark');
|
|
render(<GlMapPreview token="pk.test" style={STREETS} lat={48.8} lng={2.3} zoom={16} enable3d />);
|
|
act(() => glHandlers.load?.());
|
|
|
|
expect(addCustom3dBuildings).toHaveBeenCalledWith(glMap, true);
|
|
});
|
|
|
|
it('FE-COMP-GLPREVIEW-009: a Standard-family style skips terrain and the custom building layer', () => {
|
|
vi.mocked(isStandardFamily).mockReturnValue(true);
|
|
vi.mocked(supportsCustom3d).mockReturnValue(false);
|
|
render(<GlMapPreview token="pk.test" style={MAPBOX_DEFAULT_STYLE} lat={48.8} lng={2.3} zoom={16} enable3d />);
|
|
act(() => glHandlers.load?.());
|
|
|
|
expect(addTerrainAndSky).not.toHaveBeenCalled();
|
|
expect(addCustom3dBuildings).not.toHaveBeenCalled();
|
|
// The default style ships its own terrain, so the preview clears ours.
|
|
expect(glMap.setTerrain).toHaveBeenCalledWith(null);
|
|
});
|
|
|
|
it('FE-COMP-GLPREVIEW-010: a failing setTerrain on load is swallowed', () => {
|
|
glMap.setTerrain.mockImplementationOnce(() => {
|
|
throw new Error('no terrain');
|
|
});
|
|
render(<GlMapPreview token="pk.test" style={MAPBOX_DEFAULT_STYLE} lat={48.8} lng={2.3} zoom={16} enable3d={false} />);
|
|
|
|
expect(() => act(() => glHandlers.load?.())).not.toThrow();
|
|
});
|
|
|
|
it('FE-COMP-GLPREVIEW-011: with 3D off nothing is added on load', () => {
|
|
render(<GlMapPreview token="pk.test" style={STREETS} lat={48.8} lng={2.3} zoom={16} enable3d={false} />);
|
|
act(() => glHandlers.load?.());
|
|
|
|
expect(addTerrainAndSky).not.toHaveBeenCalled();
|
|
expect(addCustom3dBuildings).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-COMP-GLPREVIEW-012: a map click reports the picked coordinates', () => {
|
|
const onClick = vi.fn();
|
|
render(<GlMapPreview token="pk.test" style={STREETS} lat={48.8} lng={2.3} zoom={16} enable3d={false} onClick={onClick} />);
|
|
|
|
act(() => glHandlers.click?.({ lngLat: { lat: 51.5, lng: -0.12 } }));
|
|
|
|
expect(onClick).toHaveBeenCalledWith({ lat: 51.5, lng: -0.12 });
|
|
});
|
|
|
|
it('FE-COMP-GLPREVIEW-013: moving the centre recenters instead of rebuilding the map', () => {
|
|
const { rerender } = render(
|
|
<GlMapPreview token="pk.test" style={STREETS} lat={48.8} lng={2.3} zoom={16} enable3d={false} />,
|
|
);
|
|
glMap.jumpTo.mockClear();
|
|
|
|
rerender(<GlMapPreview token="pk.test" style={STREETS} lat={51.5} lng={-0.12} zoom={12} enable3d={false} />);
|
|
|
|
expect(glMap.jumpTo).toHaveBeenCalledWith({ center: [-0.12, 51.5], zoom: 12 });
|
|
expect(mapboxCtor).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('FE-COMP-GLPREVIEW-014: a failing jumpTo is swallowed and unmounting removes the map', () => {
|
|
glMap.jumpTo.mockImplementationOnce(() => {
|
|
throw new Error('not ready');
|
|
});
|
|
const { unmount } = render(
|
|
<GlMapPreview token="pk.test" style={STREETS} lat={48.8} lng={2.3} zoom={16} enable3d={false} />,
|
|
);
|
|
|
|
expect(glMap.jumpTo).toHaveBeenCalled();
|
|
glMap.remove.mockImplementationOnce(() => {
|
|
throw new Error('already gone');
|
|
});
|
|
expect(() => unmount()).not.toThrow();
|
|
expect(glMap.remove).toHaveBeenCalled();
|
|
});
|
|
});
|