From 63a98155cd4716129862cd383ecc83a24d3fedb0 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Fri, 14 Feb 2025 21:55:23 +0100 Subject: [PATCH] Add more unit tests for common/entity (#24182) * Add new entity tests * Improve canToggleDomain test --- test/common/entity/battery_icon.test.ts | 45 ++++ test/common/entity/can_toggle_domain.test.ts | 63 +++++- .../common/entity/color/battery_color.test.ts | 31 +++ test/common/entity/compute_state_name.test.ts | 56 +++++ test/common/entity/cover_icon.test.ts | 54 +++++ test/common/entity/delete_entity.test.ts | 194 ++++++++++++++++++ 6 files changed, 435 insertions(+), 8 deletions(-) create mode 100644 test/common/entity/battery_icon.test.ts create mode 100644 test/common/entity/color/battery_color.test.ts create mode 100644 test/common/entity/compute_state_name.test.ts create mode 100644 test/common/entity/cover_icon.test.ts create mode 100644 test/common/entity/delete_entity.test.ts diff --git a/test/common/entity/battery_icon.test.ts b/test/common/entity/battery_icon.test.ts new file mode 100644 index 0000000000..0d48a85d93 --- /dev/null +++ b/test/common/entity/battery_icon.test.ts @@ -0,0 +1,45 @@ +import type { HassEntity } from "home-assistant-js-websocket"; +import { describe, it, expect } from "vitest"; +import { + batteryIcon, + batteryLevelIcon, +} from "../../../src/common/entity/battery_icon"; + +describe("batteryIcon", () => { + it("should return correct icon for battery level", () => { + const stateObj: HassEntity = { state: "50" } as HassEntity; + expect(batteryIcon(stateObj)).toBe("mdi:battery-50"); + }); + + it("should return correct icon for battery level with state", () => { + const stateObj: HassEntity = { state: "50" } as HassEntity; + expect(batteryIcon(stateObj, "20")).toBe("mdi:battery-20"); + }); +}); + +describe("batteryLevelIcon", () => { + it("should return correct icon for battery level", () => { + expect(batteryLevelIcon(50)).toBe("mdi:battery-50"); + }); + + it("should return correct icon for charging battery", () => { + expect(batteryLevelIcon(50, true)).toBe("mdi:battery-charging-50"); + }); + + it("should return charging outline icon for charging battery with 9%", () => { + expect(batteryLevelIcon(9, true)).toBe("mdi:battery-charging-outline"); + }); + + it("should return alert icon for low battery", () => { + expect(batteryLevelIcon(5)).toBe("mdi:battery-alert-variant-outline"); + }); + + it("should return unknown icon for invalid battery level", () => { + expect(batteryLevelIcon("invalid")).toBe("mdi:battery-unknown"); + }); + + it("should return battery icon for on/off", () => { + expect(batteryLevelIcon("off")).toBe("mdi:battery"); + expect(batteryLevelIcon("on")).toBe("mdi:battery-alert"); + }); +}); diff --git a/test/common/entity/can_toggle_domain.test.ts b/test/common/entity/can_toggle_domain.test.ts index 410bde96b4..3ffaf9e0fa 100644 --- a/test/common/entity/can_toggle_domain.test.ts +++ b/test/common/entity/can_toggle_domain.test.ts @@ -1,6 +1,7 @@ import { assert, describe, it } from "vitest"; import { canToggleDomain } from "../../../src/common/entity/can_toggle_domain"; +import type { HomeAssistant } from "../../../src/types"; describe("canToggleDomain", () => { const hass: any = { @@ -9,10 +10,6 @@ describe("canToggleDomain", () => { turn_on: null, // Service keys only need to be present for test turn_off: null, }, - lock: { - lock: null, - unlock: null, - }, sensor: { custom_service: null, }, @@ -23,10 +20,6 @@ describe("canToggleDomain", () => { assert.isTrue(canToggleDomain(hass, "light")); }); - it("Detects locks toggle", () => { - assert.isTrue(canToggleDomain(hass, "lock")); - }); - it("Detects sensors do not toggle", () => { assert.isFalse(canToggleDomain(hass, "sensor")); }); @@ -34,4 +27,58 @@ describe("canToggleDomain", () => { it("Detects binary sensors do not toggle", () => { assert.isFalse(canToggleDomain(hass, "binary_sensor")); }); + + it("Detects covers toggle", () => { + assert.isTrue( + canToggleDomain( + { + services: { + cover: { + open_cover: null, + }, + }, + } as unknown as HomeAssistant, + "cover" + ) + ); + assert.isFalse( + canToggleDomain( + { + services: { + cover: { + open: null, + }, + }, + } as unknown as HomeAssistant, + "cover" + ) + ); + }); + + it("Detects lock toggle", () => { + assert.isTrue( + canToggleDomain( + { + services: { + lock: { + lock: null, + }, + }, + } as unknown as HomeAssistant, + "lock" + ) + ); + assert.isFalse( + canToggleDomain( + { + services: { + lock: { + unlock: null, + }, + }, + } as unknown as HomeAssistant, + "lock" + ) + ); + }); }); diff --git a/test/common/entity/color/battery_color.test.ts b/test/common/entity/color/battery_color.test.ts new file mode 100644 index 0000000000..0cdf60d73e --- /dev/null +++ b/test/common/entity/color/battery_color.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { batteryStateColorProperty } from "../../../../src/common/entity/color/battery_color"; + +describe("battery_color", () => { + it("should return green for high battery level", () => { + let color = batteryStateColorProperty("70"); + expect(color).toBe("--state-sensor-battery-high-color"); + color = batteryStateColorProperty("200"); + expect(color).toBe("--state-sensor-battery-high-color"); + }); + + it("should return yellow for medium battery level", () => { + let color = batteryStateColorProperty("69.99"); + expect(color).toBe("--state-sensor-battery-medium-color"); + color = batteryStateColorProperty("30"); + expect(color).toBe("--state-sensor-battery-medium-color"); + }); + + it("should return red for low battery level", () => { + let color = batteryStateColorProperty("29.999"); + expect(color).toBe("--state-sensor-battery-low-color"); + color = batteryStateColorProperty("-20"); + expect(color).toBe("--state-sensor-battery-low-color"); + }); + + // add nan test + it("should return undefined for non-numeric state", () => { + const color = batteryStateColorProperty("not a number"); + expect(color).toBe(undefined); + }); +}); diff --git a/test/common/entity/compute_state_name.test.ts b/test/common/entity/compute_state_name.test.ts new file mode 100644 index 0000000000..21b3f93855 --- /dev/null +++ b/test/common/entity/compute_state_name.test.ts @@ -0,0 +1,56 @@ +import type { HassEntity } from "home-assistant-js-websocket"; +import { describe, it, expect } from "vitest"; +import { + computeStateName, + computeStateNameFromEntityAttributes, +} from "../../../src/common/entity/compute_state_name"; + +describe("computeStateName", () => { + it("should return friendly_name if it exists", () => { + const stateObj = { + entity_id: "light.living_room", + attributes: { friendly_name: "Living Room Light" }, + } as HassEntity; + expect(computeStateName(stateObj)).toBe("Living Room Light"); + }); + + it("should return object id if friendly_name does not exist", () => { + const stateObj = { + entity_id: "light.living_room", + attributes: {}, + } as HassEntity; + expect(computeStateName(stateObj)).toBe("living room"); + }); +}); + +describe("computeStateNameFromEntityAttributes", () => { + it("should return friendly_name if it exists", () => { + const entityId = "light.living_room"; + const attributes = { friendly_name: "Living Room Light" }; + expect(computeStateNameFromEntityAttributes(entityId, attributes)).toBe( + "Living Room Light" + ); + }); + + it("should return friendly_name 0", () => { + const entityId = "light.living_room"; + const attributes = { friendly_name: 0 }; + expect(computeStateNameFromEntityAttributes(entityId, attributes)).toBe( + "0" + ); + }); + + it("should return empty if friendly_name is null", () => { + const entityId = "light.living_room"; + const attributes = { friendly_name: null }; + expect(computeStateNameFromEntityAttributes(entityId, attributes)).toBe(""); + }); + + it("should return object id if friendly_name does not exist", () => { + const entityId = "light.living_room"; + const attributes = {}; + expect(computeStateNameFromEntityAttributes(entityId, attributes)).toBe( + "living room" + ); + }); +}); diff --git a/test/common/entity/cover_icon.test.ts b/test/common/entity/cover_icon.test.ts new file mode 100644 index 0000000000..f075d747ea --- /dev/null +++ b/test/common/entity/cover_icon.test.ts @@ -0,0 +1,54 @@ +import type { HassEntity } from "home-assistant-js-websocket"; +import { + mdiArrowCollapseHorizontal, + mdiArrowDown, + mdiArrowExpandHorizontal, + mdiArrowUp, +} from "@mdi/js"; +import { describe, it, expect } from "vitest"; +import { + computeOpenIcon, + computeCloseIcon, +} from "../../../src/common/entity/cover_icon"; + +describe("computeOpenIcon", () => { + it("returns mdiArrowExpandHorizontal for awning, door, gate, and curtain", () => { + const stateObj = { attributes: { device_class: "awning" } } as HassEntity; + expect(computeOpenIcon(stateObj)).toBe(mdiArrowExpandHorizontal); + + stateObj.attributes.device_class = "door"; + expect(computeOpenIcon(stateObj)).toBe(mdiArrowExpandHorizontal); + + stateObj.attributes.device_class = "gate"; + expect(computeOpenIcon(stateObj)).toBe(mdiArrowExpandHorizontal); + + stateObj.attributes.device_class = "curtain"; + expect(computeOpenIcon(stateObj)).toBe(mdiArrowExpandHorizontal); + }); + + it("returns mdiArrowUp for other device classes", () => { + const stateObj = { attributes: { device_class: "window" } } as HassEntity; + expect(computeOpenIcon(stateObj)).toBe(mdiArrowUp); + }); +}); + +describe("computeCloseIcon", () => { + it("returns mdiArrowCollapseHorizontal for awning, door, gate, and curtain", () => { + const stateObj = { attributes: { device_class: "awning" } } as HassEntity; + expect(computeCloseIcon(stateObj)).toBe(mdiArrowCollapseHorizontal); + + stateObj.attributes.device_class = "door"; + expect(computeCloseIcon(stateObj)).toBe(mdiArrowCollapseHorizontal); + + stateObj.attributes.device_class = "gate"; + expect(computeCloseIcon(stateObj)).toBe(mdiArrowCollapseHorizontal); + + stateObj.attributes.device_class = "curtain"; + expect(computeCloseIcon(stateObj)).toBe(mdiArrowCollapseHorizontal); + }); + + it("returns mdiArrowDown for other device classes", () => { + const stateObj = { attributes: { device_class: "window" } } as HassEntity; + expect(computeCloseIcon(stateObj)).toBe(mdiArrowDown); + }); +}); diff --git a/test/common/entity/delete_entity.test.ts b/test/common/entity/delete_entity.test.ts new file mode 100644 index 0000000000..58f645990d --- /dev/null +++ b/test/common/entity/delete_entity.test.ts @@ -0,0 +1,194 @@ +import { describe, it, expect, vi } from "vitest"; +import { + isDeletableEntity, + deleteEntity, +} from "../../../src/common/entity/delete_entity"; +import type { HomeAssistant } from "../../../src/types"; +import type { EntityRegistryEntry } from "../../../src/data/entity_registry"; +import type { IntegrationManifest } from "../../../src/data/integration"; +import type { ConfigEntry } from "../../../src/data/config_entries"; +import type { Helper } from "../../../src/panels/config/helpers/const"; + +describe("isDeletableEntity", () => { + it("should return true for restored entities", () => { + const hass = { + states: { "light.test": { attributes: { restored: true } } }, + } as unknown as HomeAssistant; + const result = isDeletableEntity(hass, "light.test", [], [], [], []); + expect(result).toBe(true); + }); + + it("should return false for non-restored entities without config entry", () => { + const hass = { + states: { "light.test": { attributes: {} } }, + } as unknown as HomeAssistant; + const entityRegistry = [ + { entity_id: "light.test" }, + ] as EntityRegistryEntry[]; + const result = isDeletableEntity( + hass, + "light.test", + [], + entityRegistry, + [], + [] + ); + expect(result).toBe(false); + }); + + it("should return true for helper domain entities", () => { + const hass = { + states: { "input_boolean.test": { attributes: {} } }, + config: { components: ["input_boolean"] }, + } as unknown as HomeAssistant; + const entityRegistry = [ + { entity_id: "input_boolean.test", unique_id: "123" }, + ] as EntityRegistryEntry[]; + const fetchedHelpers = [{ id: "123" }] as Helper[]; + const result = isDeletableEntity( + hass, + "input_boolean.test", + [], + entityRegistry, + [], + fetchedHelpers + ); + expect(result).toBe(true); + }); + + it("should return false for non-helper domain entities without restored attribute", () => { + const hass = { + states: { "light.test": { attributes: {} } }, + } as unknown as HomeAssistant; + const entityRegistry = [ + { entity_id: "light.test" }, + ] as EntityRegistryEntry[]; + const result = isDeletableEntity( + hass, + "light.test", + [], + entityRegistry, + [], + [] + ); + expect(result).toBe(false); + }); + + it("should return true for entities with helper integration type", () => { + const hass = { + states: { "light.test": { attributes: {} } }, + } as unknown as HomeAssistant; + const entityRegistry = [ + { entity_id: "light.test", config_entry_id: "config_1" }, + ] as EntityRegistryEntry[]; + const configEntries = [ + { entry_id: "config_1", domain: "light" }, + ] as ConfigEntry[]; + const manifests = [ + { domain: "light", integration_type: "helper" }, + ] as IntegrationManifest[]; + const result = isDeletableEntity( + hass, + "light.test", + manifests, + entityRegistry, + configEntries, + [] + ); + expect(result).toBe(true); + }); +}); + +describe("deleteEntity", () => { + it("should call removeEntityRegistryEntry for restored entities", () => { + const removeEntityRegistryEntry = vi.fn(); + const hass = { + states: { "light.test": { attributes: { restored: true } } }, + callWS: removeEntityRegistryEntry, + } as unknown as HomeAssistant; + const entityRegistry = [ + { entity_id: "light.test" }, + ] as EntityRegistryEntry[]; + deleteEntity(hass, "light.test", [], entityRegistry, [], []); + expect(removeEntityRegistryEntry).toHaveBeenCalledWith({ + type: "config/entity_registry/remove", + entity_id: "light.test", + }); + }); + + it("should call deleteConfigEntry for entities with helper integration type", () => { + const deleteConfigEntry = vi.fn(); + const hass = { + states: { "light.test": { attributes: {} } }, + callApi: deleteConfigEntry, + } as unknown as HomeAssistant; + const entityRegistry = [ + { entity_id: "light.test", config_entry_id: "config_1" }, + ] as EntityRegistryEntry[]; + const configEntries = [ + { entry_id: "config_1", domain: "light" }, + ] as ConfigEntry[]; + const manifests = [ + { domain: "light", integration_type: "helper" }, + ] as IntegrationManifest[]; + deleteEntity( + hass, + "light.test", + manifests, + entityRegistry, + configEntries, + [] + ); + expect(deleteConfigEntry).toHaveBeenCalledOnce(); + }); + + it("should call HELPERS_CRUD.delete for helper domain entities", () => { + const deleteCall = vi.fn(); + const hass = { + states: { "input_boolean.test": { attributes: {} } }, + config: { components: ["input_boolean"] }, + callWS: deleteCall, + } as unknown as HomeAssistant; + const entityRegistry = [ + { entity_id: "input_boolean.test", unique_id: "123" }, + ] as EntityRegistryEntry[]; + const fetchedHelpers = [{ id: "123" }] as Helper[]; + deleteEntity( + hass, + "input_boolean.test", + [], + entityRegistry, + [], + fetchedHelpers + ); + expect(deleteCall).toHaveBeenCalledWith({ + type: "input_boolean/delete", + input_boolean_id: "123", + }); + }); + + it("should call removeEntityRegistryEntry for helper domain entities", () => { + const removeEntityRegistryEntry = vi.fn(); + const hass = { + states: { "input_boolean.test": { attributes: { restored: true } } }, + config: { components: ["input_boolean"] }, + callWS: removeEntityRegistryEntry, + } as unknown as HomeAssistant; + const entityRegistry = [ + { entity_id: "input_boolean.test", unique_id: "124" }, + ] as EntityRegistryEntry[]; + const fetchedHelpers = [{ id: "123" }] as Helper[]; + deleteEntity( + hass, + "input_boolean.test", + [], + entityRegistry, + [], + fetchedHelpers + ); + expect(removeEntityRegistryEntry).toHaveBeenCalledWith({ + type: "config/entity_registry/remove", + entity_id: "input_boolean.test", + }); + }); +});