diff --git a/packages/core/src/compiler/htmlBundler.test.ts b/packages/core/src/compiler/htmlBundler.test.ts index b820d536fa..3a4f86da70 100644 --- a/packages/core/src/compiler/htmlBundler.test.ts +++ b/packages/core/src/compiler/htmlBundler.test.ts @@ -3,8 +3,9 @@ import { mkdtempSync, writeFileSync, mkdirSync, symlinkSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { parseHTML } from "linkedom"; -import { describe, it, expect, vi } from "vitest"; +import { afterEach, beforeEach, describe, it, expect, vi } from "vitest"; import { bundleToSingleHtml } from "./htmlBundler"; +import { resetUnknownEnumWarnings } from "../runtime/getVariables"; import { getHyperframeRuntimeScript } from "../generated/runtime-inline"; function makeTempProject(files: Record): string { @@ -1388,3 +1389,124 @@ describe("bundleToSingleHtml", () => { } }); }); + +/** + * A sub-composition given a value outside a declared enum's `options` falls + * back silently. The runtime guard in getVariables.ts cannot see it: the + * bundler bakes the per-instance values into `window.__hfVariablesByComp` at + * compile time and the sub-comp's scoped `getVariables` shim only reads that + * table. Compile time is therefore the only place the defect is observable on + * this path, so the same warning is emitted here. + */ +describe("bundleToSingleHtml unknown enum values", () => { + let warnings: string[]; + + beforeEach(() => { + resetUnknownEnumWarnings(); + warnings = []; + vi.spyOn(console, "warn").mockImplementation((...args: unknown[]) => { + warnings.push(args.map(String).join(" ")); + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + resetUnknownEnumWarnings(); + }); + + const enumWarnings = () => warnings.filter((w) => w.includes("runtime_unknown_enum_value")); + + const ACCENT_ENUM = + '[{"id":"accent","type":"enum","label":"Accent","default":"green","options":["green","blue","violet"]}]'; + + function makeSubCompProject(variableValues: string, declaration = ACCENT_ENUM): string { + return makeTempProject({ + "index.html": ` + +
+
+
+ +`, + "compositions/card.html": ` + + +
+ +`, + }); + } + + it("warns when a sub-composition instance value is not a declared option", async () => { + await bundleToSingleHtml(makeSubCompProject('{"accent":"orange"}')); + + expect(enumWarnings()).toEqual([ + '[hyperframes] runtime_unknown_enum_value: card variable "accent" got "orange", ' + + "which is not a declared option (green, blue, violet). " + + 'Rendering "green" instead.', + ]); + }); + + it("is silent when the instance value is a declared option", async () => { + await bundleToSingleHtml(makeSubCompProject('{"accent":"violet"}')); + + expect(enumWarnings()).toEqual([]); + }); + + it("never inspects a variable declared without options", async () => { + const declaration = '[{"id":"accent","type":"string","label":"Accent","default":"green"}]'; + await bundleToSingleHtml(makeSubCompProject('{"accent":"orange"}', declaration)); + + expect(enumWarnings()).toEqual([]); + }); + + it("is silent for a declared enum absent from the instance values", async () => { + await bundleToSingleHtml(makeSubCompProject('{"unrelated":"whatever"}')); + + expect(enumWarnings()).toEqual([]); + }); + + it("passes the unknown value through to the bundle unrewritten", async () => { + const bundled = await bundleToSingleHtml(makeSubCompProject('{"accent":"orange"}')); + + expect(bundled).toContain("window.__hfVariablesByComp = Object.assign({}, "); + expect(bundled).toContain('{ "card": { "accent": "orange" } }'); + expect(bundled).toMatch(/\[data-composition-id="card"\]\s*\{[^}]*--accent:\s*orange/); + expect(bundled).not.toContain("--accent: green"); + }); + + it("warns once for the same composition, variable and value across bundles", async () => { + const dir = makeSubCompProject('{"accent":"orange"}'); + await bundleToSingleHtml(dir); + await bundleToSingleHtml(dir); + + expect(enumWarnings()).toHaveLength(1); + }); + + it("warns for a