mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-13 09:59:12 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e95634e77 | ||
|
|
91a6d737b3 | ||
|
|
22c3a6fe67 | ||
|
|
bcc799970a | ||
|
|
31d4a37c15 |
@@ -472,10 +472,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
const newValue = value?.trim();
|
||||
const newTab = ev.ctrlKey || ev.metaKey;
|
||||
|
||||
this._fireSelectedEvents(newValue, index, newTab);
|
||||
this._fireSelectedEvents(value, index, newTab);
|
||||
};
|
||||
|
||||
private _fireSelectedEvents(value: string, index: number, newTab = false) {
|
||||
|
||||
@@ -94,6 +94,29 @@ const localizeTimeString = (
|
||||
}
|
||||
};
|
||||
|
||||
// Seconds since midnight for a literal `HH:MM(:SS)` time, undefined for
|
||||
// anything else (entity ids contain a dot, and malformed input is ignored).
|
||||
const literalTimeToSeconds = (value: unknown): number | undefined => {
|
||||
if (typeof value !== "string" || value.includes(".")) {
|
||||
return undefined;
|
||||
}
|
||||
const chunks = value.split(":");
|
||||
if (chunks.length < 2 || chunks.length > 3) {
|
||||
return undefined;
|
||||
}
|
||||
const hours = Number(chunks[0]);
|
||||
const minutes = Number(chunks[1]);
|
||||
const seconds = chunks.length > 2 ? Number(chunks[2]) : 0;
|
||||
if (
|
||||
!Number.isFinite(hours) ||
|
||||
!Number.isFinite(minutes) ||
|
||||
!Number.isFinite(seconds)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
};
|
||||
|
||||
const formatNumericLimitValue = (
|
||||
hass: HomeAssistant,
|
||||
value?: number | string
|
||||
@@ -1232,12 +1255,16 @@ const describeLegacyCondition = (
|
||||
|
||||
let hasTime = "";
|
||||
if (after !== undefined && before !== undefined) {
|
||||
if (
|
||||
typeof condition.after === "string" &&
|
||||
!condition.after.includes(".") &&
|
||||
typeof condition.before === "string" &&
|
||||
!condition.before.includes(".") &&
|
||||
condition.after > condition.before
|
||||
const afterSeconds = literalTimeToSeconds(condition.after);
|
||||
const beforeSeconds = literalTimeToSeconds(condition.before);
|
||||
if (beforeSeconds === 0) {
|
||||
// A window ending at midnight runs to the end of the day, so the
|
||||
// "before" boundary adds nothing to the summary.
|
||||
hasTime = "after";
|
||||
} else if (
|
||||
afterSeconds !== undefined &&
|
||||
beforeSeconds !== undefined &&
|
||||
afterSeconds > beforeSeconds
|
||||
) {
|
||||
hasTime = "after_before_or";
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { timeCacheEntityPromiseFunc } from "../common/util/time-cache-entity-promise-func";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { MediaPlayerItem, SearchMediaResult } from "./media-player";
|
||||
|
||||
@@ -7,7 +8,7 @@ export interface ResolvedMediaSource {
|
||||
}
|
||||
|
||||
export const resolveMediaSource = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
media_content_id: string
|
||||
) =>
|
||||
hass.callWS<ResolvedMediaSource>({
|
||||
@@ -15,6 +16,23 @@ export const resolveMediaSource = (
|
||||
media_content_id,
|
||||
});
|
||||
|
||||
// Resolved URLs are signed and valid for 24 hours (CONTENT_AUTH_EXPIRY_TIME in
|
||||
// core). Resolving again returns a different signature, which would defeat the
|
||||
// browser cache, so reuse the resolved URL for just under its validity.
|
||||
export const RESOLVE_CACHE_TIME = 23 * 60 * 60 * 1000; // 23 hours
|
||||
|
||||
export const resolveMediaSourceWithCache = (
|
||||
hass: Pick<HomeAssistant, "callWS" | "hassUrl">,
|
||||
media_content_id: string
|
||||
): Promise<ResolvedMediaSource> =>
|
||||
timeCacheEntityPromiseFunc(
|
||||
"_resolvedMediaSource",
|
||||
RESOLVE_CACHE_TIME,
|
||||
resolveMediaSource,
|
||||
hass,
|
||||
media_content_id
|
||||
);
|
||||
|
||||
export const browseLocalMediaPlayer = (
|
||||
hass: HomeAssistant,
|
||||
mediaContentId?: string
|
||||
|
||||
@@ -622,6 +622,8 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
var(--ha-color-border-neutral-quiet);
|
||||
overflow: hidden;
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
}
|
||||
.target.warning {
|
||||
background: var(--ha-color-fill-warning-normal-resting);
|
||||
|
||||
@@ -622,8 +622,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
}
|
||||
),
|
||||
confirmText: this.hass!.localize(
|
||||
"ui.panel.config.blueprint.overview.blueprint_in_use_view",
|
||||
{ type }
|
||||
`ui.panel.config.blueprint.overview.blueprint_in_use_view_${blueprint.domain}`
|
||||
),
|
||||
});
|
||||
if (result) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
|
||||
import {
|
||||
isMediaSourceContentId,
|
||||
resolveMediaSource,
|
||||
resolveMediaSourceWithCache,
|
||||
} from "../../../../data/media_source";
|
||||
|
||||
@customElement("hui-view-background-editor")
|
||||
@@ -136,21 +136,35 @@ export class HuiViewBackgroundEditor extends LitElement {
|
||||
`${(background.opacity ?? 100) / 100}`
|
||||
);
|
||||
|
||||
const backgroundImage =
|
||||
typeof background.image === "object"
|
||||
? background.image.media_content_id
|
||||
: background.image;
|
||||
const backgroundImage = this._currentBackgroundImage();
|
||||
|
||||
if (backgroundImage && isMediaSourceContentId(backgroundImage)) {
|
||||
resolveMediaSource(this.hass, backgroundImage).then((result) => {
|
||||
this._resolvedImage = result.url;
|
||||
});
|
||||
resolveMediaSourceWithCache(this.hass, backgroundImage).then(
|
||||
(result) => {
|
||||
// Discard if the image changed while resolving
|
||||
if (this._currentBackgroundImage() === backgroundImage) {
|
||||
this._resolvedImage = result.url;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
if (this._currentBackgroundImage() === backgroundImage) {
|
||||
this._resolvedImage = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this._resolvedImage = backgroundImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _currentBackgroundImage(): string | undefined {
|
||||
const background = this._backgroundData(this._config);
|
||||
return typeof background.image === "object"
|
||||
? background.image.media_content_id
|
||||
: background.image;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceViewBackgroundConfig } from "../../../data/lovelace/config/view";
|
||||
import {
|
||||
isMediaSourceContentId,
|
||||
resolveMediaSource,
|
||||
resolveMediaSourceWithCache,
|
||||
} from "../../../data/media_source";
|
||||
|
||||
@customElement("hui-view-background")
|
||||
@@ -21,20 +21,37 @@ export class HUIViewBackground extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _fetchMedia() {
|
||||
const backgroundImage =
|
||||
typeof this.background === "string"
|
||||
? this.background
|
||||
: typeof this.background?.image === "object"
|
||||
? this.background.image.media_content_id
|
||||
: this.background?.image;
|
||||
private _getBackgroundImage(
|
||||
background?: string | LovelaceViewBackgroundConfig
|
||||
): string | undefined {
|
||||
if (typeof background === "string") {
|
||||
return background;
|
||||
}
|
||||
if (typeof background?.image === "object") {
|
||||
return background.image.media_content_id;
|
||||
}
|
||||
return background?.image;
|
||||
}
|
||||
|
||||
if (backgroundImage && isMediaSourceContentId(backgroundImage)) {
|
||||
resolveMediaSource(this.hass, backgroundImage).then((result) => {
|
||||
this.resolvedImage = result.url;
|
||||
});
|
||||
} else {
|
||||
private async _fetchMedia() {
|
||||
const backgroundImage = this._getBackgroundImage(this.background);
|
||||
|
||||
if (!backgroundImage || !isMediaSourceContentId(backgroundImage)) {
|
||||
this.resolvedImage = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
let resolvedUrl: string | undefined;
|
||||
try {
|
||||
resolvedUrl = (
|
||||
await resolveMediaSourceWithCache(this.hass, backgroundImage)
|
||||
).url;
|
||||
} catch {
|
||||
resolvedUrl = undefined;
|
||||
}
|
||||
// Discard if the background changed while resolving
|
||||
if (this._getBackgroundImage(this.background) === backgroundImage) {
|
||||
this.resolvedImage = resolvedUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +90,7 @@ export class HUIViewBackground extends LitElement {
|
||||
background?: string | LovelaceViewBackgroundConfig
|
||||
) {
|
||||
if (typeof background === "object" && background.image) {
|
||||
const image =
|
||||
typeof background.image === "object"
|
||||
? background.image.media_content_id || ""
|
||||
: background.image;
|
||||
const image = this._getBackgroundImage(background) || "";
|
||||
if (isMediaSourceContentId(image) && !this.resolvedImage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6131,7 +6131,8 @@
|
||||
"error": "{path} could not be loaded",
|
||||
"blueprint_in_use_title": "This blueprint is in use and cannot be deleted",
|
||||
"blueprint_in_use_text": "Please remove all below {type} that use this blueprint before deleting it. {list}",
|
||||
"blueprint_in_use_view": "view {type}",
|
||||
"blueprint_in_use_view_automation": "View automations",
|
||||
"blueprint_in_use_view_script": "View scripts",
|
||||
"confirm_delete_title": "Delete blueprint?",
|
||||
"confirm_delete_text": "{name} will be permanently deleted.",
|
||||
"add_blueprint": "Import blueprint",
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
RESOLVE_CACHE_TIME,
|
||||
resolveMediaSourceWithCache,
|
||||
} from "../../src/data/media_source";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
const CONTENT_ID = "media-source://image_upload/background";
|
||||
const OTHER_CONTENT_ID = "media-source://image_upload/other";
|
||||
|
||||
// Core signs every resolution with a fresh timestamp, so an uncached resolve of
|
||||
// the same id yields a different url each time. The mock reproduces that: the
|
||||
// urls only stay equal if the resolution itself was reused.
|
||||
const mockHass = () => {
|
||||
let signature = 0;
|
||||
return {
|
||||
callWS: vi.fn(({ media_content_id }: { media_content_id: string }) => {
|
||||
signature += 1;
|
||||
return Promise.resolve({
|
||||
url: `/api/image/serve/${media_content_id.split("/").pop()}/original?authSig=sig${signature}`,
|
||||
mime_type: "image/jpeg",
|
||||
});
|
||||
}),
|
||||
} as unknown as HomeAssistant;
|
||||
};
|
||||
|
||||
describe("resolveMediaSourceWithCache", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("returns the same url when the same content id is resolved again", async () => {
|
||||
const hass = mockHass();
|
||||
|
||||
const first = await resolveMediaSourceWithCache(hass, CONTENT_ID);
|
||||
const second = await resolveMediaSourceWithCache(hass, CONTENT_ID);
|
||||
|
||||
expect(second.url).toBe(first.url);
|
||||
expect(hass.callWS).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("shares a single request between concurrent callers", async () => {
|
||||
const hass = mockHass();
|
||||
|
||||
const [first, second] = await Promise.all([
|
||||
resolveMediaSourceWithCache(hass, CONTENT_ID),
|
||||
resolveMediaSourceWithCache(hass, CONTENT_ID),
|
||||
]);
|
||||
|
||||
expect(second.url).toBe(first.url);
|
||||
expect(hass.callWS).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("resolves each content id to its own url", async () => {
|
||||
const hass = mockHass();
|
||||
|
||||
const first = await resolveMediaSourceWithCache(hass, CONTENT_ID);
|
||||
const other = await resolveMediaSourceWithCache(hass, OTHER_CONTENT_ID);
|
||||
|
||||
expect(first.url).toContain("/background/");
|
||||
expect(other.url).toContain("/other/");
|
||||
expect(hass.callWS).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("keeps returning the same url when hass is updated", async () => {
|
||||
const hass = mockHass();
|
||||
|
||||
const first = await resolveMediaSourceWithCache(hass, CONTENT_ID);
|
||||
// State updates replace hass with a shallow copy
|
||||
const second = await resolveMediaSourceWithCache(
|
||||
{ ...hass } as HomeAssistant,
|
||||
CONTENT_ID
|
||||
);
|
||||
|
||||
expect(second.url).toBe(first.url);
|
||||
expect(hass.callWS).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not cache failures", async () => {
|
||||
const hass = mockHass();
|
||||
vi.mocked(hass.callWS).mockRejectedValueOnce(new Error("unresolvable"));
|
||||
|
||||
await expect(
|
||||
resolveMediaSourceWithCache(hass, CONTENT_ID)
|
||||
).rejects.toThrowError("unresolvable");
|
||||
const retried = await resolveMediaSourceWithCache(hass, CONTENT_ID);
|
||||
|
||||
expect(retried.url).toContain("authSig=");
|
||||
expect(hass.callWS).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("resolves a fresh url once the cached one is about to expire", async () => {
|
||||
const hass = mockHass();
|
||||
|
||||
const first = await resolveMediaSourceWithCache(hass, CONTENT_ID);
|
||||
vi.advanceTimersByTime(RESOLVE_CACHE_TIME);
|
||||
const second = await resolveMediaSourceWithCache(hass, CONTENT_ID);
|
||||
|
||||
expect(second.url).not.toBe(first.url);
|
||||
expect(hass.callWS).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("caches for less than the 24 hour signature validity", () => {
|
||||
expect(RESOLVE_CACHE_TIME).toBeLessThan(24 * 60 * 60 * 1000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { IntlMessageFormat } from "intl-messageformat";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describeCondition } from "../../src/data/automation_i18n";
|
||||
import {
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
TimeZone,
|
||||
} from "../../src/data/translation";
|
||||
import en from "../../src/translations/en.json";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
type TranslationNode = string | { [key: string]: TranslationNode };
|
||||
|
||||
const localize = (key: string, values?: Record<string, unknown>) => {
|
||||
const message = key
|
||||
.split(".")
|
||||
.reduce<TranslationNode | undefined>(
|
||||
(translations, part) =>
|
||||
typeof translations === "object" ? translations[part] : undefined,
|
||||
en as TranslationNode
|
||||
);
|
||||
return typeof message === "string"
|
||||
? (new IntlMessageFormat(message, "en").format(values) as string)
|
||||
: "";
|
||||
};
|
||||
|
||||
const hass = {
|
||||
localize,
|
||||
locale: {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.twenty_four,
|
||||
date_format: DateFormat.language,
|
||||
first_weekday: FirstWeekday.language,
|
||||
time_zone: TimeZone.local,
|
||||
},
|
||||
config: { time_zone: "Etc/UTC" },
|
||||
states: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const describeTimeCondition = (after?: string, before?: string) =>
|
||||
describeCondition({ condition: "time", after, before }, hass, []);
|
||||
|
||||
describe("time condition description", () => {
|
||||
it("joins a window within one day with 'and'", () => {
|
||||
expect(describeTimeCondition("09:00:00", "17:00:00")).toBe(
|
||||
"If the time is after 09:00 and before 17:00"
|
||||
);
|
||||
});
|
||||
|
||||
it("joins a window crossing midnight with 'or'", () => {
|
||||
expect(describeTimeCondition("22:00:00", "06:00:00")).toBe(
|
||||
"If the time is after 22:00 or before 06:00"
|
||||
);
|
||||
});
|
||||
|
||||
it("omits a 'before' boundary of midnight, which ends the window at the end of the day", () => {
|
||||
expect(describeTimeCondition("10:00:00", "00:00:00")).toBe(
|
||||
"If the time is after 10:00"
|
||||
);
|
||||
});
|
||||
|
||||
it("compares times numerically, not lexicographically", () => {
|
||||
expect(describeTimeCondition("9:00:00", "10:00:00")).toBe(
|
||||
"If the time is after 09:00 and before 10:00"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not compare entity references", () => {
|
||||
expect(describeTimeCondition("input_datetime.wake_up", "10:00:00")).toBe(
|
||||
"If the time is after entity input_datetime.wake_up and before 10:00"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import "../../../../src/panels/lovelace/views/hui-view-background";
|
||||
import type { HUIViewBackground } from "../../../../src/panels/lovelace/views/hui-view-background";
|
||||
import type { LovelaceViewBackgroundConfig } from "../../../../src/data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
const IMAGE_A = "media-source://image_upload/a";
|
||||
const IMAGE_B = "media-source://image_upload/b";
|
||||
|
||||
// Resolutions are answered by hand so a slow one can land after a later,
|
||||
// already-resolved one — the ordering a cache hit makes easy to hit.
|
||||
const deferredHass = () => {
|
||||
const pending = new Map<string, (url: string) => void>();
|
||||
const hass = {
|
||||
callWS: vi.fn(
|
||||
({ media_content_id }: { media_content_id: string }) =>
|
||||
new Promise((resolve) => {
|
||||
pending.set(media_content_id, (url: string) =>
|
||||
resolve({ url, mime_type: "image/jpeg" })
|
||||
);
|
||||
})
|
||||
),
|
||||
hassUrl: (path?: string) => path ?? "",
|
||||
} as unknown as HomeAssistant;
|
||||
return { hass, pending };
|
||||
};
|
||||
|
||||
let elements: HUIViewBackground[] = [];
|
||||
|
||||
const mount = async (
|
||||
hass: HomeAssistant,
|
||||
background: string | LovelaceViewBackgroundConfig
|
||||
) => {
|
||||
const el = document.createElement("hui-view-background") as HUIViewBackground;
|
||||
el.hass = hass;
|
||||
el.background = background;
|
||||
document.body.appendChild(el);
|
||||
elements.push(el);
|
||||
await el.updateComplete;
|
||||
return el;
|
||||
};
|
||||
|
||||
const setBackground = async (
|
||||
el: HUIViewBackground,
|
||||
background: string | LovelaceViewBackgroundConfig
|
||||
) => {
|
||||
el.background = background;
|
||||
await el.updateComplete;
|
||||
};
|
||||
|
||||
// Let the resolve chain drain before checking what was applied
|
||||
const settle = async (el: HUIViewBackground) => {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
await el.updateComplete;
|
||||
};
|
||||
|
||||
const imageBackground = (
|
||||
mediaContentId: string
|
||||
): LovelaceViewBackgroundConfig => ({
|
||||
image: { media_content_id: mediaContentId },
|
||||
});
|
||||
|
||||
const backgroundUrl = (el: HUIViewBackground) =>
|
||||
el.style.getPropertyValue("--view-background");
|
||||
|
||||
afterEach(() => {
|
||||
elements.forEach((el) => el.remove());
|
||||
elements = [];
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("hui-view-background", () => {
|
||||
it("applies the resolved url of a media source background", async () => {
|
||||
const { hass, pending } = deferredHass();
|
||||
const el = await mount(hass, imageBackground(IMAGE_A));
|
||||
|
||||
pending.get(IMAGE_A)!("/a.jpg?authSig=a");
|
||||
await settle(el);
|
||||
|
||||
expect(backgroundUrl(el)).toContain("/a.jpg?authSig=a");
|
||||
});
|
||||
|
||||
it("ignores a resolution that arrives after the background changed", async () => {
|
||||
const { hass, pending } = deferredHass();
|
||||
const el = await mount(hass, imageBackground(IMAGE_A));
|
||||
await setBackground(el, imageBackground(IMAGE_B));
|
||||
|
||||
// B resolves first, then the stale A resolution lands
|
||||
pending.get(IMAGE_B)!("/b.jpg?authSig=b");
|
||||
await settle(el);
|
||||
pending.get(IMAGE_A)!("/a.jpg?authSig=a");
|
||||
await settle(el);
|
||||
|
||||
expect(backgroundUrl(el)).toContain("/b.jpg?authSig=b");
|
||||
expect(backgroundUrl(el)).not.toContain("/a.jpg");
|
||||
});
|
||||
|
||||
it("keeps a plain css background untouched", async () => {
|
||||
const { hass } = deferredHass();
|
||||
const el = await mount(hass, "#3f51b5");
|
||||
|
||||
expect(hass.callWS).not.toHaveBeenCalled();
|
||||
expect(backgroundUrl(el)).toBe("#3f51b5");
|
||||
});
|
||||
|
||||
it("clears the resolved image when the background is replaced by a color", async () => {
|
||||
const { hass, pending } = deferredHass();
|
||||
const el = await mount(hass, imageBackground(IMAGE_A));
|
||||
pending.get(IMAGE_A)!("/a.jpg?authSig=a");
|
||||
await settle(el);
|
||||
|
||||
await setBackground(el, "#3f51b5");
|
||||
|
||||
expect(backgroundUrl(el)).toBe("#3f51b5");
|
||||
});
|
||||
|
||||
it("falls back to the theme background when resolving fails", async () => {
|
||||
const hass = {
|
||||
callWS: vi.fn().mockRejectedValue(new Error("unresolvable")),
|
||||
hassUrl: (path?: string) => path ?? "",
|
||||
} as unknown as HomeAssistant;
|
||||
const el = await mount(hass, imageBackground(IMAGE_A));
|
||||
await settle(el);
|
||||
|
||||
expect(backgroundUrl(el)).toBe("");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user