Files
frontend/test/components/map/ha-map.test.ts
088f72fcc4 Fix map loading fully zoomed out when fitted before layout (#53428)
Leaflet computes the zoom level that fits a set of bounds from the current
size of the map container. When ha-map's fit runs before the browser has
laid out the container (a race that hits when the Leaflet chunks are
already cached), the container measures 0x0, the computed zoom collapses
to the minimum, and the map shows the entire world. The later
invalidateSize() from the resize observer fixes the size but keeps the
zoom, so the map stays on the world view.

Defer fitting while the container has no size and run the pending fit
once the resize observer reports a usable size.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 06:52:29 +00:00

144 lines
4.1 KiB
TypeScript

import type { HassEntities } from "home-assistant-js-websocket";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import "../../../src/components/map/ha-map";
import type { HaMap } from "../../../src/components/map/ha-map";
type ROCallback = (
entries: ResizeObserverEntry[],
observer: ResizeObserver
) => void;
const resizeCallbacks: ROCallback[] = [];
class MockResizeObserver {
constructor(cb: ROCallback) {
resizeCallbacks.push(cb);
}
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
}
const STATES = {
"device_tracker.paulus": {
entity_id: "device_tracker.paulus",
state: "not_home",
attributes: {
friendly_name: "Paulus",
latitude: 52.372,
longitude: 4.89,
},
context: { id: "1", user_id: null, parent_id: null },
last_changed: "2026-01-01T00:00:00Z",
last_updated: "2026-01-01T00:00:00Z",
},
"device_tracker.anne_therese": {
entity_id: "device_tracker.anne_therese",
state: "not_home",
attributes: {
friendly_name: "Anne Therese",
latitude: 52.377,
longitude: 4.895,
},
context: { id: "2", user_id: null, parent_id: null },
last_changed: "2026-01-01T00:00:00Z",
last_updated: "2026-01-01T00:00:00Z",
},
} as unknown as HassEntities;
const createMap = async (): Promise<HaMap> => {
const el = document.createElement("ha-map");
el.entities = [
"device_tracker.paulus",
"device_tracker.anne_therese",
] as string[];
el.clusterMarkers = false;
(el as any)._states = STATES;
(el as any)._config = {
config: { latitude: 52.3731339, longitude: 4.8903147 },
};
document.body.appendChild(el);
await vi.waitUntil(() => el.leafletMap !== undefined && (el as any)._loaded);
await el.updateComplete;
return el;
};
const setMapSize = (el: HaMap, width: number, height: number) => {
const mapDiv = el.shadowRoot!.getElementById("map")!;
Object.defineProperty(mapDiv, "clientWidth", {
value: width,
configurable: true,
});
Object.defineProperty(mapDiv, "clientHeight", {
value: height,
configurable: true,
});
};
const fireResizeObservers = () => {
resizeCallbacks.forEach((cb) => cb([], {} as ResizeObserver));
};
describe("ha-map", () => {
beforeEach(() => {
resizeCallbacks.length = 0;
vi.stubGlobal("ResizeObserver", MockResizeObserver);
});
afterEach(() => {
document.body.innerHTML = "";
vi.unstubAllGlobals();
});
it("fits the map to its entities once the container is laid out", async () => {
// While the container still has a 0x0 size (before browser layout),
// Leaflet cannot compute a fit zoom.
const el = await createMap();
// Container gets its size and the resize observer fires, as happens
// when the browser completes layout after the map loaded.
setMapSize(el, 800, 500);
fireResizeObservers();
await el.updateComplete;
const map = el.leafletMap!;
// The map should be fitted to the markers, not zoomed out to the world.
expect(map.getZoom()).toBeGreaterThanOrEqual(10);
expect(map.getCenter().lat).toBeCloseTo(52.3745, 2);
expect(map.getCenter().lng).toBeCloseTo(4.8925, 2);
});
it("does not defer fitting when the container already has a size", async () => {
const originalWidth = Object.getOwnPropertyDescriptor(
Element.prototype,
"clientWidth"
);
const originalHeight = Object.getOwnPropertyDescriptor(
Element.prototype,
"clientHeight"
);
Object.defineProperty(Element.prototype, "clientWidth", {
get: () => 800,
configurable: true,
});
Object.defineProperty(Element.prototype, "clientHeight", {
get: () => 500,
configurable: true,
});
try {
const el = await createMap();
const map = el.leafletMap!;
expect(map.getZoom()).toBeGreaterThanOrEqual(10);
expect(map.getCenter().lat).toBeCloseTo(52.3745, 2);
expect(map.getCenter().lng).toBeCloseTo(4.8925, 2);
} finally {
Object.defineProperty(Element.prototype, "clientWidth", originalWidth!);
Object.defineProperty(Element.prototype, "clientHeight", originalHeight!);
}
});
});