mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-17 02:23:22 +01:00
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>
144 lines
4.1 KiB
TypeScript
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!);
|
|
}
|
|
});
|
|
});
|