mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
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.
121 lines
3.8 KiB
TypeScript
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();
|
|
});
|
|
});
|