diff --git a/src/create-thing.ts b/src/create-thing.ts index a87df12..4af4746 100644 --- a/src/create-thing.ts +++ b/src/create-thing.ts @@ -91,8 +91,13 @@ export const createThing = (cardConfig, isRow = false) => { const timer = setTimeout(() => { element.style.display = ""; }, 2000); - // Remove error if element is defined later - customElements.whenDefined(cardConfig.type).then(() => { + // Remove error if element is defined later. Must wait on `tag` (the resolved custom element + // tag name, e.g. "hui-conditional-row" or "multiple-entity-row"), not `cardConfig.type` (the + // raw config type string, e.g. "conditional" or "custom:multiple-entity-row") - those are + // different strings, so waiting on cardConfig.type would never resolve, and for any `custom:` + // type it can never resolve at all (colons aren't valid in custom element names). See + // https://github.com/custom-cards/button-card/pull/1152 for the same fix in a fork of this file. + customElements.whenDefined(tag).then(() => { clearTimeout(timer); fireEvent(element, "ll-rebuild", {}, element); }); diff --git a/test/create-thing.test.ts b/test/create-thing.test.ts new file mode 100644 index 0000000..fafbf70 --- /dev/null +++ b/test/create-thing.test.ts @@ -0,0 +1,72 @@ +/** @vitest-environment jsdom */ +import { describe, expect, it } from "vitest"; +import { createThing } from "../src/create-thing"; + +// See https://github.com/custom-cards/button-card/pull/1152 - the "wait for the lazy-loaded +// element, then recover" mechanism was waiting on `customElements.whenDefined(cardConfig.type)` +// (the raw config type string, e.g. "custom:my-card") instead of the actual resolved DOM tag +// (e.g. "my-card"). Those are different strings, so the wait never resolved - for a `custom:` +// type it could never resolve at all, since colons aren't valid in custom element names. +// +// createThing's error path itself renders a "hui-error-card" element (real HA always has this +// registered) - stub it here so _createThing's error-card creation succeeds in this isolated +// test environment. +if (!customElements.get("hui-error-card")) { + class HuiErrorCard extends HTMLElement { + setConfig() {} + } + customElements.define("hui-error-card", HuiErrorCard); +} + +describe("createThing recovery", () => { + it("fires ll-rebuild once the resolved tag is defined, for a custom: card type", async () => { + const tag = `test-recovers-card-${Math.random().toString(36).slice(2)}`; + + const element = createThing({ type: `custom:${tag}` }); + expect(element.tagName.toLowerCase()).toBe("hui-error-card"); + + let rebuilt = false; + element.addEventListener("ll-rebuild", () => { + rebuilt = true; + }); + + class TestCard extends HTMLElement { + setConfig() {} + } + customElements.define(tag, TestCard); + + await customElements.whenDefined(tag); + // ll-rebuild is fired synchronously in the whenDefined().then() callback, which runs as a + // microtask - flush the microtask queue once more before asserting. + await Promise.resolve(); + + expect(rebuilt).toBe(true); + }); + + it("fires ll-rebuild once the resolved tag is defined, for a built-in card type", async () => { + // Matches the actual reported scenario (button-card#1150): a built-in "conditional" card + // nested as a custom_field, whose type string ("conditional") has no hyphen and is never a + // valid custom element name on its own - only the resolved "hui-conditional-card" tag is. + const tag = "hui-conditional-card"; + + const element = createThing({ type: "conditional" }); + expect(element.tagName.toLowerCase()).toBe("hui-error-card"); + + let rebuilt = false; + element.addEventListener("ll-rebuild", () => { + rebuilt = true; + }); + + if (!customElements.get(tag)) { + class ConditionalCard extends HTMLElement { + setConfig() {} + } + customElements.define(tag, ConditionalCard); + } + + await customElements.whenDefined(tag); + await Promise.resolve(); + + expect(rebuilt).toBe(true); + }); +});