1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-05-18 14:09:05 +01:00
Files
frontend/test/data/compute-navigation-path-info.test.ts
Paulus Schoutsen e66564ff65 Add apps group to navigation picker with all web UI addons (#51572)
* Add apps navigation group with ingress add-on panels

Add an "Apps" section to the navigation picker that shows all add-ons
with ingress support. Uses the /ingress/panels supervisor endpoint via
a cached collection to fetch add-on titles and icons in a single call.

https://claude.ai/code/session_01F8dUzfSWj8ZwDByVZ45BNj

* Fix no-shadow lint error for panels variable

Rename subscribe callback parameter from `panels` to `data` to avoid
shadowing the outer `panels` variable in _loadNavigationItems.

https://claude.ai/code/session_01F8dUzfSWj8ZwDByVZ45BNj

* Use subscribeOne helper for ingress panels collection

Replace hand-rolled Promise/subscribe/unsub pattern with the existing
subscribeOne utility for cleaner one-shot collection consumption.

https://claude.ai/code/session_01F8dUzfSWj8ZwDByVZ45BNj

* Add explicit type parameter to subscribeOne call

TypeScript cannot infer the generic type through the collection
subscribe chain, resulting in unknown type for panel entries.

https://claude.ai/code/session_01F8dUzfSWj8ZwDByVZ45BNj

* Add subscribeOneCollection helper for collection one-shot reads

Add a new subscribeOneCollection utility that takes a collection
directly instead of requiring the (conn, onChange) function pattern.
Use it in the navigation picker for cleaner ingress panel fetching.

https://claude.ai/code/session_01F8dUzfSWj8ZwDByVZ45BNj

* Use Collection type instead of custom Subscribable interface

https://claude.ai/code/session_01F8dUzfSWj8ZwDByVZ45BNj

* Add ingress panel support to subscribeNavigationPathInfo

* Use app panel variable

* Add tests

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2026-04-20 08:28:01 +03:00

347 lines
9.6 KiB
TypeScript

import { mdiDevices, mdiLink, mdiPuzzle, mdiTextureBox } from "@mdi/js";
import { describe, expect, it } from "vitest";
import { computeNavigationPathInfo } from "../../src/data/compute-navigation-path-info";
import type { IngressPanelInfoMap } from "../../src/data/hassio/ingress";
import type { HomeAssistant } from "../../src/types";
import type { LovelaceConfig } from "../../src/data/lovelace/config/types";
const createHass = (overrides: Partial<HomeAssistant> = {}): HomeAssistant =>
({
panels: {},
areas: {},
devices: {},
localize: () => "",
...overrides,
}) as unknown as HomeAssistant;
describe("computeNavigationPathInfo", () => {
describe("panel paths", () => {
it("resolves a panel with icon", () => {
const hass = createHass({
panels: {
"my-panel": {
url_path: "my-panel",
title: "My Panel",
icon: "mdi:star",
component_name: "custom",
},
} as unknown as HomeAssistant["panels"],
});
const result = computeNavigationPathInfo(hass, "/my-panel");
expect(result.label).toBe("My Panel");
expect(result.icon).toBe("mdi:star");
expect(result.iconPath).toBe(mdiLink);
});
it("falls back to url path for unknown panel", () => {
const hass = createHass();
const result = computeNavigationPathInfo(hass, "/unknown");
expect(result.label).toBe("unknown");
expect(result.icon).toBeUndefined();
expect(result.iconPath).toBe(mdiLink);
});
});
describe("area paths", () => {
it("resolves /config/areas/area/{areaId}", () => {
const hass = createHass({
areas: {
living_room: {
area_id: "living_room",
name: "Living Room",
icon: "mdi:sofa",
},
} as unknown as HomeAssistant["areas"],
});
const result = computeNavigationPathInfo(
hass,
"/config/areas/area/living_room"
);
expect(result.label).toBe("Living Room");
expect(result.icon).toBe("mdi:sofa");
expect(result.iconPath).toBe(mdiTextureBox);
});
it("resolves /home/areas-{areaId}", () => {
const hass = createHass({
areas: {
kitchen: {
area_id: "kitchen",
name: "Kitchen",
icon: null,
},
} as unknown as HomeAssistant["areas"],
});
const result = computeNavigationPathInfo(hass, "/home/areas-kitchen");
expect(result.label).toBe("Kitchen");
expect(result.icon).toBeUndefined();
expect(result.iconPath).toBe(mdiTextureBox);
});
it("falls back to area id for unknown area", () => {
const hass = createHass();
const result = computeNavigationPathInfo(
hass,
"/config/areas/area/unknown_area"
);
expect(result.label).toBe("unknown_area");
expect(result.iconPath).toBe(mdiTextureBox);
});
});
describe("device paths", () => {
it("resolves /config/devices/device/{deviceId}", () => {
const hass = createHass({
devices: {
abc123: {
id: "abc123",
name: "Smart Light",
name_by_user: null,
},
} as unknown as HomeAssistant["devices"],
});
const result = computeNavigationPathInfo(
hass,
"/config/devices/device/abc123"
);
expect(result.label).toBe("Smart Light");
expect(result.iconPath).toBe(mdiDevices);
});
it("prefers user-defined device name", () => {
const hass = createHass({
devices: {
abc123: {
id: "abc123",
name: "Smart Light",
name_by_user: "My Light",
},
} as unknown as HomeAssistant["devices"],
});
const result = computeNavigationPathInfo(
hass,
"/config/devices/device/abc123"
);
expect(result.label).toBe("My Light");
});
it("falls back to device id for unknown device", () => {
const hass = createHass();
const result = computeNavigationPathInfo(
hass,
"/config/devices/device/unknown_device"
);
expect(result.label).toBe("unknown_device");
expect(result.iconPath).toBe(mdiDevices);
});
});
describe("lovelace view paths", () => {
const lovelaceConfig: LovelaceConfig = {
views: [
{ title: "Overview", path: "overview", icon: "mdi:home" },
{ title: "Lights", path: "lights" },
{ path: "my-view" },
{},
],
};
it("resolves view with title and icon", () => {
const hass = createHass({
panels: {
lovelace: {
url_path: "lovelace",
title: "Dashboard",
component_name: "lovelace",
},
} as unknown as HomeAssistant["panels"],
});
const result = computeNavigationPathInfo(
hass,
"/lovelace/overview",
lovelaceConfig
);
expect(result.label).toBe("Overview");
expect(result.icon).toBe("mdi:home");
});
it("resolves view without icon using default", () => {
const hass = createHass({
panels: {
lovelace: {
url_path: "lovelace",
title: "Dashboard",
component_name: "lovelace",
},
} as unknown as HomeAssistant["panels"],
});
const result = computeNavigationPathInfo(
hass,
"/lovelace/lights",
lovelaceConfig
);
expect(result.label).toBe("Lights");
expect(result.icon).toBe("mdi:view-compact");
});
it("uses titleCase of path when view has no title", () => {
const hass = createHass({
panels: {
lovelace: {
url_path: "lovelace",
title: "Dashboard",
component_name: "lovelace",
},
} as unknown as HomeAssistant["panels"],
});
const result = computeNavigationPathInfo(
hass,
"/lovelace/my-view",
lovelaceConfig
);
expect(result.label).toBe("My-view");
});
it("uses index as name when view has no title or path", () => {
const hass = createHass({
panels: {
lovelace: {
url_path: "lovelace",
title: "Dashboard",
component_name: "lovelace",
},
} as unknown as HomeAssistant["panels"],
});
const result = computeNavigationPathInfo(
hass,
"/lovelace/3",
lovelaceConfig
);
expect(result.label).toBe("3");
});
it("falls back to panel info when view not found", () => {
const hass = createHass({
panels: {
lovelace: {
url_path: "lovelace",
title: "Dashboard",
component_name: "lovelace",
},
} as unknown as HomeAssistant["panels"],
});
const result = computeNavigationPathInfo(
hass,
"/lovelace/nonexistent",
lovelaceConfig
);
expect(result.label).toBe("Dashboard");
});
it("falls back to panel info when no lovelace config provided", () => {
const hass = createHass({
panels: {
lovelace: {
url_path: "lovelace",
title: "Dashboard",
component_name: "lovelace",
},
} as unknown as HomeAssistant["panels"],
});
const result = computeNavigationPathInfo(hass, "/lovelace/overview");
expect(result.label).toBe("Dashboard");
});
});
describe("ingress panel paths", () => {
const ingressPanels: IngressPanelInfoMap = {
my_addon: {
title: "My Addon",
icon: "mdi:puzzle",
},
no_icon_addon: {
title: "No Icon Addon",
icon: "",
},
};
it("resolves /app/<slug> with ingress panels data", () => {
const hass = createHass();
const result = computeNavigationPathInfo(
hass,
"/app/my_addon",
undefined,
ingressPanels
);
expect(result.label).toBe("My Addon");
expect(result.icon).toBe("mdi:puzzle");
expect(result.iconPath).toBe(mdiPuzzle);
});
it("falls back to slug when ingress panels not provided", () => {
const hass = createHass();
const result = computeNavigationPathInfo(hass, "/app/my_addon");
expect(result.label).toBe("my_addon");
expect(result.icon).toBeUndefined();
expect(result.iconPath).toBe(mdiPuzzle);
});
it("falls back to slug when addon not found in ingress panels", () => {
const hass = createHass();
const result = computeNavigationPathInfo(
hass,
"/app/unknown_addon",
undefined,
ingressPanels
);
expect(result.label).toBe("unknown_addon");
expect(result.icon).toBeUndefined();
expect(result.iconPath).toBe(mdiPuzzle);
});
it("resolves addon with empty icon as undefined", () => {
const hass = createHass();
const result = computeNavigationPathInfo(
hass,
"/app/no_icon_addon",
undefined,
ingressPanels
);
expect(result.label).toBe("No Icon Addon");
expect(result.icon).toBeUndefined();
expect(result.iconPath).toBe(mdiPuzzle);
});
it("does not resolve /app without a slug", () => {
const hass = createHass({
panels: {
app: {
url_path: "app",
title: "Apps",
component_name: "app",
},
} as unknown as HomeAssistant["panels"],
});
const result = computeNavigationPathInfo(
hass,
"/app",
undefined,
ingressPanels
);
// Falls through to panel resolution, not ingress
expect(result.label).toBe("Apps");
});
});
});