Files
hyperframes/packages/core/src/runtime/adapters/mapbox.test.ts
T
Miguel Ángel 75d92b55e0 feat(core): add readiness adapters for map and visualization libraries (#1548)
Add readiness-only runtime adapters for Mapbox GL JS, Leaflet, Google
Maps, MapLibre GL JS, and D3. Each adapter gates `__renderReady` until
the library's async initialization completes, preventing the renderer
from capturing blank or half-loaded frames.

Built on the `getReadyPromise` adapter contract from #1543. A shared
`createReadinessAdapter()` helper in `_readiness.ts` owns the
settled-tracking WeakSet, promise-identity stability, and
`Promise.allSettled` gate — each adapter provides only its type, window
global name, and `waitFor` callback.

Readiness signals per library:
- Mapbox / MapLibre: `map.loaded()` + `map.on('load', ...)`
- Leaflet: `map.whenReady(cb)`
- Google Maps: `map.addListener('tilesloaded', cb)` with handle cleanup
- D3: `transition.end()` promise

50 unit tests across 5 test files covering happy path, no-instances,
stable promise identity, post-settle drain, loaded-before-subscribe
race, and listener cleanup. 5 producer regression tests with
Docker-generated baselines for end-to-end render verification.
2026-06-18 01:21:27 -04:00

121 lines
3.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createMapboxAdapter } from "./mapbox";
type MapboxMapLike = {
loaded: () => boolean;
on: (event: string, cb: () => void) => void;
};
const mapWindow = window as Window & { __hfMapbox?: MapboxMapLike[] };
function createMockMap(opts?: { loaded?: boolean }): MapboxMapLike {
const listeners: Record<string, (() => void)[]> = {};
return {
loaded: vi.fn(() => opts?.loaded ?? false),
on: vi.fn((event: string, cb: () => void) => {
(listeners[event] ??= []).push(cb);
}),
_fire(event: string) {
for (const cb of listeners[event] ?? []) cb();
},
} as MapboxMapLike & { _fire: (e: string) => void };
}
describe("mapbox adapter", () => {
beforeEach(() => {
delete mapWindow.__hfMapbox;
});
afterEach(() => {
delete mapWindow.__hfMapbox;
});
it("has correct name", () => {
expect(createMapboxAdapter().name).toBe("mapbox");
});
describe("getReadyPromise", () => {
it("returns null when no maps registered", () => {
const adapter = createMapboxAdapter();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("returns null when __hfMapbox is empty", () => {
mapWindow.__hfMapbox = [];
const adapter = createMapboxAdapter();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("resolves when map fires load event", async () => {
const map = createMockMap() as MapboxMapLike & { _fire: (e: string) => void };
mapWindow.__hfMapbox = [map];
const adapter = createMapboxAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
map._fire("load");
await promise;
});
it("resolves immediately for already-loaded map", async () => {
const map = createMockMap({ loaded: true });
mapWindow.__hfMapbox = [map];
const adapter = createMapboxAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
await promise;
});
it("returns same promise on repeated calls (stable identity)", () => {
const map = createMockMap();
mapWindow.__hfMapbox = [map];
const adapter = createMapboxAdapter();
const p1 = adapter.getReadyPromise!();
const p2 = adapter.getReadyPromise!();
expect(p1).toBe(p2);
});
it("returns null after all maps have settled", async () => {
const map = createMockMap({ loaded: true });
mapWindow.__hfMapbox = [map];
const adapter = createMapboxAdapter();
await adapter.getReadyPromise!();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("handles mix of loaded and unloaded maps", async () => {
const loaded = createMockMap({ loaded: true });
const unloaded = createMockMap() as MapboxMapLike & { _fire: (e: string) => void };
mapWindow.__hfMapbox = [loaded, unloaded];
const adapter = createMapboxAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
unloaded._fire("load");
await promise;
expect(adapter.getReadyPromise!()).toBeNull();
});
it("resolves via loaded() check when map finishes loading before subscribe", async () => {
const racyMap = {
loaded: vi.fn(() => true),
on: vi.fn(),
};
mapWindow.__hfMapbox = [racyMap];
const adapter = createMapboxAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
await promise;
expect(racyMap.on).not.toHaveBeenCalled();
});
});
it("discover is a no-op", () => {
const adapter = createMapboxAdapter();
expect(() => adapter.discover()).not.toThrow();
});
it("seek is a no-op", () => {
const adapter = createMapboxAdapter();
expect(() => adapter.seek({ time: 1 })).not.toThrow();
});
});