Add more tests for common/entity (#24336)
* Use substring instead of deprecated substr * Add more common entity testspull/24343/head
parent
400106ec09
commit
0a05dd8f71
|
@ -226,6 +226,7 @@
|
|||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.24.1",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.0.5",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
export const computeDomain = (entityId: string): string =>
|
||||
entityId.substr(0, entityId.indexOf("."));
|
||||
entityId.substring(0, entityId.indexOf("."));
|
||||
|
|
|
@ -120,11 +120,6 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||
return value;
|
||||
}
|
||||
|
||||
if (domain === "datetime") {
|
||||
const time = new Date(state);
|
||||
return formatDateTime(time, locale, config);
|
||||
}
|
||||
|
||||
if (["date", "input_datetime", "time"].includes(domain)) {
|
||||
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
|
||||
// Attributes aren't available, we have to use `state`.
|
||||
|
@ -181,6 +176,7 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
].includes(domain) ||
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
) {
|
||||
|
|
|
@ -63,4 +63,28 @@ describe("canToggleState", () => {
|
|||
};
|
||||
assert.isFalse(canToggleState(hass, stateObj));
|
||||
});
|
||||
|
||||
it("Detects group with missing entity", () => {
|
||||
const stateObj: any = {
|
||||
entity_id: "group.bla",
|
||||
state: "on",
|
||||
attributes: {
|
||||
entity_id: ["light.non_existing"],
|
||||
},
|
||||
};
|
||||
|
||||
assert.isFalse(canToggleState(hass, stateObj));
|
||||
});
|
||||
|
||||
it("Detects group with off state", () => {
|
||||
const stateObj: any = {
|
||||
entity_id: "group.bla",
|
||||
state: "off",
|
||||
attributes: {
|
||||
entity_id: ["light.test"],
|
||||
},
|
||||
};
|
||||
|
||||
assert.isTrue(canToggleState(hass, stateObj));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,371 @@
|
|||
import type {
|
||||
HassConfig,
|
||||
HassEntity,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
computeAttributeValueDisplay,
|
||||
computeAttributeNameDisplay,
|
||||
} from "../../../src/common/entity/compute_attribute_display";
|
||||
import type { FrontendLocaleData } from "../../../src/data/translation";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
|
||||
export const localizeMock = (key: string) => {
|
||||
const translations = {
|
||||
"state.default.unknown": "Unknown",
|
||||
"component.test_platform.entity.sensor.test_translation_key.state_attributes.attribute.state.42":
|
||||
"42",
|
||||
"component.test_platform.entity.sensor.test_translation_key.state_attributes.attribute.state.attributeValue":
|
||||
"Localized Attribute Name",
|
||||
"component.media_player.entity_component.media_player.state_attributes.attribute.state.attributeValue":
|
||||
"Localized Media Player Attribute Name",
|
||||
"component.media_player.entity_component._.state_attributes.attribute.state.attributeValue":
|
||||
"Media Player Attribute Name",
|
||||
};
|
||||
return translations[key] || "";
|
||||
};
|
||||
|
||||
export const stateObjMock = {
|
||||
entity_id: "sensor.test",
|
||||
attributes: {
|
||||
device_class: "temperature",
|
||||
},
|
||||
} as HassEntityBase;
|
||||
|
||||
export const localeMock = {
|
||||
language: "en",
|
||||
} as FrontendLocaleData;
|
||||
|
||||
export const configMock = {
|
||||
unit_system: {
|
||||
temperature: "°C",
|
||||
},
|
||||
} as HassConfig;
|
||||
|
||||
export const entitiesMock = {
|
||||
"sensor.test": {
|
||||
platform: "test_platform",
|
||||
translation_key: "test_translation_key",
|
||||
},
|
||||
"media_player.test": {
|
||||
platform: "media_player",
|
||||
},
|
||||
} as unknown as HomeAssistant["entities"];
|
||||
|
||||
describe("computeAttributeValueDisplay", () => {
|
||||
it("should return unknown state for null value", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
null
|
||||
);
|
||||
expect(result).toBe("Unknown");
|
||||
});
|
||||
|
||||
it("should return formatted number for numeric value", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
42
|
||||
);
|
||||
expect(result).toBe("42");
|
||||
});
|
||||
|
||||
it("should return number from formatter", () => {
|
||||
const stateObj = {
|
||||
entity_id: "media_player.test",
|
||||
attributes: {
|
||||
device_class: "media_player",
|
||||
volume_level: 0.42,
|
||||
},
|
||||
} as unknown as HassEntityBase;
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObj,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"volume_level"
|
||||
);
|
||||
expect(result).toBe("42%");
|
||||
});
|
||||
|
||||
it("should return formatted date for date string", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
"2023-10-10"
|
||||
);
|
||||
expect(result).toBe("October 10, 2023");
|
||||
});
|
||||
|
||||
it("should return formatted datetime for timestamp", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
"2023-10-10T10:10:10"
|
||||
);
|
||||
expect(result).toBe("October 10, 2023 at 10:10:10");
|
||||
});
|
||||
|
||||
it("should return JSON string for object value", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
{ key: "value" }
|
||||
);
|
||||
expect(result).toBe('{"key":"value"}');
|
||||
});
|
||||
|
||||
it("should return concatenated values for array", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
[1, 2, 3]
|
||||
);
|
||||
expect(result).toBe("1, 2, 3");
|
||||
});
|
||||
|
||||
it("should set special unit for weather domain", () => {
|
||||
const stateObj = {
|
||||
entity_id: "weather.test",
|
||||
attributes: {
|
||||
temperature: 42,
|
||||
},
|
||||
} as unknown as HassEntityBase;
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObj,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"temperature"
|
||||
);
|
||||
expect(result).toBe("42 °C");
|
||||
});
|
||||
|
||||
it("should set temperature unit for temperature attribute", () => {
|
||||
const stateObj = {
|
||||
entity_id: "sensor.test",
|
||||
attributes: {
|
||||
temperature: 42,
|
||||
},
|
||||
} as unknown as HassEntityBase;
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObj,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"temperature"
|
||||
);
|
||||
expect(result).toBe("42 °C");
|
||||
});
|
||||
|
||||
it("should return translation from translation key", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
"attributeValue"
|
||||
);
|
||||
expect(result).toBe("Localized Attribute Name");
|
||||
});
|
||||
|
||||
it("should return device class translation", () => {
|
||||
const stateObj = {
|
||||
entity_id: "media_player.test",
|
||||
attributes: {
|
||||
device_class: "media_player",
|
||||
volume_level: 0.42,
|
||||
},
|
||||
} as unknown as HassEntityBase;
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObj,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
"attributeValue"
|
||||
);
|
||||
expect(result).toBe("Localized Media Player Attribute Name");
|
||||
});
|
||||
|
||||
it("should return attribute value translation", () => {
|
||||
const stateObj = {
|
||||
entity_id: "media_player.test",
|
||||
attributes: {
|
||||
volume_level: 0.42,
|
||||
},
|
||||
} as unknown as HassEntityBase;
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObj,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
"attributeValue"
|
||||
);
|
||||
expect(result).toBe("Media Player Attribute Name");
|
||||
});
|
||||
|
||||
it("should return attribute value", () => {
|
||||
const stateObj = {
|
||||
entity_id: "media_player.test",
|
||||
attributes: {
|
||||
volume_level: 0.42,
|
||||
},
|
||||
} as unknown as HassEntityBase;
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObj,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
"attributeValue2"
|
||||
);
|
||||
expect(result).toBe("attributeValue2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeAttributeNameDisplay", () => {
|
||||
it("should return localized name for attribute", () => {
|
||||
const localize = (key: string) => {
|
||||
if (
|
||||
key ===
|
||||
"component.light.entity.light.entity_translation_key.state_attributes.updated_at.name"
|
||||
) {
|
||||
return "Updated at";
|
||||
}
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
const stateObj = {
|
||||
entity_id: "light.test",
|
||||
attributes: {
|
||||
device_class: "light",
|
||||
},
|
||||
} as HassEntity;
|
||||
|
||||
const entities = {
|
||||
"light.test": {
|
||||
translation_key: "entity_translation_key",
|
||||
platform: "light",
|
||||
},
|
||||
} as unknown as HomeAssistant["entities"];
|
||||
|
||||
const result = computeAttributeNameDisplay(
|
||||
localize,
|
||||
stateObj,
|
||||
entities,
|
||||
"updated_at"
|
||||
);
|
||||
expect(result).toBe("Updated at");
|
||||
});
|
||||
|
||||
it("should return device class translation", () => {
|
||||
const localize = (key: string) => {
|
||||
if (
|
||||
key ===
|
||||
"component.light.entity_component.light.state_attributes.brightness.name"
|
||||
) {
|
||||
return "Brightness";
|
||||
}
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
const stateObj = {
|
||||
entity_id: "light.test",
|
||||
attributes: {
|
||||
device_class: "light",
|
||||
},
|
||||
} as HassEntity;
|
||||
|
||||
const entities = {} as unknown as HomeAssistant["entities"];
|
||||
|
||||
const result = computeAttributeNameDisplay(
|
||||
localize,
|
||||
stateObj,
|
||||
entities,
|
||||
"brightness"
|
||||
);
|
||||
expect(result).toBe("Brightness");
|
||||
});
|
||||
|
||||
it("should return default attribute name", () => {
|
||||
const localize = (key: string) => {
|
||||
if (
|
||||
key ===
|
||||
"component.light.entity_component._.state_attributes.brightness.name"
|
||||
) {
|
||||
return "Brightness";
|
||||
}
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
const stateObj = {
|
||||
entity_id: "light.test",
|
||||
attributes: {},
|
||||
} as HassEntity;
|
||||
|
||||
const entities = {} as unknown as HomeAssistant["entities"];
|
||||
|
||||
const result = computeAttributeNameDisplay(
|
||||
localize,
|
||||
stateObj,
|
||||
entities,
|
||||
"brightness"
|
||||
);
|
||||
expect(result).toBe("Brightness");
|
||||
});
|
||||
|
||||
it("should return capitalized attribute name", () => {
|
||||
const localize = () => "";
|
||||
|
||||
const stateObj = {
|
||||
entity_id: "light.test",
|
||||
attributes: {},
|
||||
} as HassEntity;
|
||||
|
||||
const entities = {} as unknown as HomeAssistant["entities"];
|
||||
|
||||
const result = computeAttributeNameDisplay(
|
||||
localize,
|
||||
stateObj,
|
||||
entities,
|
||||
"brightness__ip_id_mac_gps_GPS"
|
||||
);
|
||||
expect(result).toBe("Brightness IP ID MAC GPS GPS");
|
||||
});
|
||||
});
|
|
@ -1,5 +1,9 @@
|
|||
import { assert, describe, it, beforeEach } from "vitest";
|
||||
import { computeStateDisplay } from "../../../src/common/entity/compute_state_display";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import { assert, describe, it, beforeEach, expect } from "vitest";
|
||||
import {
|
||||
computeStateDisplay,
|
||||
computeStateDisplayFromEntityAttributes,
|
||||
} from "../../../src/common/entity/compute_state_display";
|
||||
import { UNKNOWN } from "../../../src/data/entity";
|
||||
import type { FrontendLocaleData } from "../../../src/data/translation";
|
||||
import {
|
||||
|
@ -10,6 +14,7 @@ import {
|
|||
TimeZone,
|
||||
} from "../../../src/data/translation";
|
||||
import { demoConfig } from "../../../src/fake_data/demo_config";
|
||||
import type { EntityRegistryDisplayEntry } from "../../../src/data/entity_registry";
|
||||
|
||||
let localeData: FrontendLocaleData;
|
||||
|
||||
|
@ -617,3 +622,85 @@ describe("computeStateDisplay", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeStateDisplayFromEntityAttributes with numeric device classes", () => {
|
||||
it("Should format duration sensor", () => {
|
||||
const result = computeStateDisplayFromEntityAttributes(
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
(() => {}) as any,
|
||||
{
|
||||
language: "en",
|
||||
} as FrontendLocaleData,
|
||||
[],
|
||||
{} as HassConfig,
|
||||
{
|
||||
display_precision: 2,
|
||||
} as EntityRegistryDisplayEntry,
|
||||
"number.test",
|
||||
{
|
||||
device_class: "duration",
|
||||
unit_of_measurement: "min",
|
||||
},
|
||||
"12"
|
||||
);
|
||||
expect(result).toBe("12.00 min");
|
||||
});
|
||||
it("Should format duration sensor with seconds", () => {
|
||||
const result = computeStateDisplayFromEntityAttributes(
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
(() => {}) as any,
|
||||
{
|
||||
language: "en",
|
||||
} as FrontendLocaleData,
|
||||
[],
|
||||
{} as HassConfig,
|
||||
undefined,
|
||||
"number.test",
|
||||
{
|
||||
device_class: "duration",
|
||||
unit_of_measurement: "s",
|
||||
},
|
||||
"12"
|
||||
);
|
||||
expect(result).toBe("12 s");
|
||||
});
|
||||
|
||||
it("Should format monetary device_class", () => {
|
||||
const result = computeStateDisplayFromEntityAttributes(
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
(() => {}) as any,
|
||||
{
|
||||
language: "en",
|
||||
} as FrontendLocaleData,
|
||||
[],
|
||||
{} as HassConfig,
|
||||
undefined,
|
||||
"number.test",
|
||||
{
|
||||
device_class: "monetary",
|
||||
unit_of_measurement: "$",
|
||||
},
|
||||
"12"
|
||||
);
|
||||
expect(result).toBe("12 $");
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeStateDisplayFromEntityAttributes datetime device calss", () => {
|
||||
it("Should format datetime sensor", () => {
|
||||
const result = computeStateDisplayFromEntityAttributes(
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
(() => {}) as any,
|
||||
{
|
||||
language: "en",
|
||||
} as FrontendLocaleData,
|
||||
[],
|
||||
{} as HassConfig,
|
||||
undefined,
|
||||
"button.test",
|
||||
{},
|
||||
"2020-01-01T12:00:00+00:00"
|
||||
);
|
||||
expect(result).toBe("January 1, 2020 at 12:00");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths()],
|
||||
test: {
|
||||
environment: "jsdom", // to run in browser-like environment
|
||||
env: {
|
||||
|
|
|
@ -49,6 +49,36 @@
|
|||
"./node_modules/@lrnwebcomponents/simple-tooltip/custom-elements.json"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"paths": {
|
||||
"lit/static-html": ["./node_modules/lit/static-html.js"],
|
||||
"lit/decorators": ["./node_modules/lit/decorators.js"],
|
||||
"lit/directive": ["./node_modules/lit/directive.js"],
|
||||
"lit/directives/until": ["./node_modules/lit/directives/until.js"],
|
||||
"lit/directives/class-map": [
|
||||
"./node_modules/lit/directives/class-map.js"
|
||||
],
|
||||
"lit/directives/style-map": [
|
||||
"./node_modules/lit/directives/style-map.js"
|
||||
],
|
||||
"lit/directives/if-defined": [
|
||||
"./node_modules/lit/directives/if-defined.js"
|
||||
],
|
||||
"lit/directives/guard": ["./node_modules/lit/directives/guard.js"],
|
||||
"lit/directives/cache": ["./node_modules/lit/directives/cache.js"],
|
||||
"lit/directives/repeat": ["./node_modules/lit/directives/repeat.js"],
|
||||
"lit/directives/live": ["./node_modules/lit/directives/live.js"],
|
||||
"lit/directives/keyed": ["./node_modules/lit/directives/keyed.js"],
|
||||
"lit/polyfill-support": ["./node_modules/lit/polyfill-support.js"],
|
||||
"@lit-labs/virtualizer/layouts/grid": [
|
||||
"./node_modules/@lit-labs/virtualizer/layouts/grid.js"
|
||||
],
|
||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver": [
|
||||
"./node_modules/@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js"
|
||||
],
|
||||
"@lit-labs/observers/resize-controller": [
|
||||
"./node_modules/@lit-labs/observers/resize-controller.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
38
yarn.lock
38
yarn.lock
|
@ -9067,6 +9067,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"globrex@npm:^0.1.2":
|
||||
version: 0.1.2
|
||||
resolution: "globrex@npm:0.1.2"
|
||||
checksum: 10/81ce62ee6f800d823d6b7da7687f841676d60ee8f51f934ddd862e4057316d26665c4edc0358d4340a923ac00a514f8b67c787e28fe693aae16350f4e60d55e9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glogg@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "glogg@npm:2.2.0"
|
||||
|
@ -9489,6 +9496,7 @@ __metadata:
|
|||
ua-parser-js: "npm:2.0.2"
|
||||
vis-data: "npm:7.1.9"
|
||||
vis-network: "npm:9.1.9"
|
||||
vite-tsconfig-paths: "npm:5.1.4"
|
||||
vitest: "npm:3.0.5"
|
||||
vue: "npm:2.7.16"
|
||||
vue2-daterange-picker: "npm:0.6.8"
|
||||
|
@ -14462,6 +14470,20 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tsconfck@npm:^3.0.3":
|
||||
version: 3.1.5
|
||||
resolution: "tsconfck@npm:3.1.5"
|
||||
peerDependencies:
|
||||
typescript: ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
bin:
|
||||
tsconfck: bin/tsconfck.js
|
||||
checksum: 10/46b68f0fcec7da045490e427400c2a7fea67bdb6dae871257e8d2ec48e9dc99674214df86ff51c6d01ceb68ba9d7d806d3b69de432efa3c76b5fba160c252857
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tsconfig-paths@npm:^3.15.0":
|
||||
version: 3.15.0
|
||||
resolution: "tsconfig-paths@npm:3.15.0"
|
||||
|
@ -15110,6 +15132,22 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite-tsconfig-paths@npm:5.1.4":
|
||||
version: 5.1.4
|
||||
resolution: "vite-tsconfig-paths@npm:5.1.4"
|
||||
dependencies:
|
||||
debug: "npm:^4.1.1"
|
||||
globrex: "npm:^0.1.2"
|
||||
tsconfck: "npm:^3.0.3"
|
||||
peerDependencies:
|
||||
vite: "*"
|
||||
peerDependenciesMeta:
|
||||
vite:
|
||||
optional: true
|
||||
checksum: 10/b409dbd17829f560021a71dba3e473b9c06dcf5fdc9d630b72c1f787145ec478b38caff1be04868971ac8bdcbf0f5af45eeece23dbc9c59c54b901f867740ae0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite@npm:^5.0.0 || ^6.0.0":
|
||||
version: 6.0.11
|
||||
resolution: "vite@npm:6.0.11"
|
||||
|
|
Loading…
Reference in New Issue