Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/create-thing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
72 changes: 72 additions & 0 deletions test/create-thing.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});