Files
hyperframes/packages/core/src/runtime/validateVariables.test.ts
T
Vance IngallsandClaude Opus 4.8 dd6fad6bb2 fix(sdk): code-review follow-ups (WS-B/C/3.C, #1569/#1570/#1572) (#1588)
Addresses the still-outstanding review concerns from merged PRs #1569 / #1570 /
#1572 not already hoisted into #1573.

WS-B (#1569):
- validateVariables requires discriminant fields for object-valued font/image
  ({name,source} / {url}); a {name:42} font or {foo:42} image previously passed
  runtime validation and surfaced as a bogus font-family / missing image.
- Dropped ImageValue's [key:string]:unknown index signature (let any {url}-shaped
  object through, swallowed typos); explicit alt?/fit? instead.
- Documented the OverrideSet widening for SDK consumers.

WS-C (#1570):
- getElementTimings caches parsed GSAP labels by exact script text (avoids a full
  acorn re-parse per read; content-key invalidates on edit).
- Documented end-inclusive label window + best-effort extractGsapLabels catch.

WS-3.C (#1572):
- Added typed Composition.addWithKeyframes / replaceWithKeyframes (was asymmetric
  with addGsapTween; Studio had to use raw dispatch).
- Extracted shared KeyframeSpec type; documented position as seconds/number-only.

Gates: build + core 18/18 + sdk 19/19 + oxlint + oxfmt + fallow + typecheck all green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 00:07:53 -07:00

177 lines
5.7 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { validateVariables, formatVariableValidationIssue } from "./validateVariables";
import type { CompositionVariable } from "../core.types";
const DECLS: readonly CompositionVariable[] = [
{ id: "title", type: "string", label: "Title", default: "Hello" },
{ id: "count", type: "number", label: "Count", default: 0 },
{ id: "active", type: "boolean", label: "Active", default: true },
{ id: "color", type: "color", label: "Color", default: "#000000" },
{
id: "theme",
type: "enum",
label: "Theme",
default: "light",
options: [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
],
},
{ id: "brandFont", type: "font", label: "Font", default: "Inter" },
{ id: "hero", type: "image", label: "Hero", default: "https://x/y.png" },
];
describe("validateVariables", () => {
it("returns no issues when every value matches its declaration", () => {
expect(
validateVariables(
{ title: "Q4", count: 3, active: false, color: "#abcdef", theme: "dark" },
DECLS,
),
).toEqual([]);
});
it("returns no issues for an empty values map", () => {
expect(validateVariables({}, DECLS)).toEqual([]);
});
it("flags undeclared keys", () => {
expect(validateVariables({ title: "x", extra: 1 }, DECLS)).toEqual([
{ kind: "undeclared", variableId: "extra" },
]);
});
it("flags string vs number mismatches", () => {
expect(validateVariables({ count: "three" }, DECLS)).toEqual([
{ kind: "type-mismatch", variableId: "count", expected: "number", actual: "string" },
]);
});
it("flags non-finite numbers as type mismatches", () => {
expect(validateVariables({ count: Number.NaN }, DECLS)).toEqual([
{ kind: "type-mismatch", variableId: "count", expected: "number", actual: "number" },
]);
});
it("flags boolean mismatches", () => {
expect(validateVariables({ active: "true" }, DECLS)).toEqual([
{ kind: "type-mismatch", variableId: "active", expected: "boolean", actual: "string" },
]);
});
it("flags non-string color values", () => {
expect(validateVariables({ color: 0xff0000 }, DECLS)).toEqual([
{ kind: "type-mismatch", variableId: "color", expected: "color", actual: "number" },
]);
});
it("flags enum values not in the allowed set", () => {
expect(validateVariables({ theme: "midnight" }, DECLS)).toEqual([
{
kind: "enum-out-of-range",
variableId: "theme",
allowed: ["light", "dark"],
actual: "midnight",
},
]);
});
it("flags non-string enum values as type mismatches", () => {
expect(validateVariables({ theme: 1 }, DECLS)).toEqual([
{ kind: "type-mismatch", variableId: "theme", expected: "enum (string)", actual: "number" },
]);
});
it("accepts a valid font object and a fallback string", () => {
expect(
validateVariables({ brandFont: { name: "Inter", source: "https://f/inter.css" } }, DECLS),
).toEqual([]);
expect(validateVariables({ brandFont: "Inter" }, DECLS)).toEqual([]);
});
it("flags a font object missing string name/source", () => {
const expected = {
kind: "type-mismatch",
variableId: "brandFont",
expected: "font object {name: string, source: string}",
actual: "object missing string name/source",
};
expect(validateVariables({ brandFont: { name: 42 } }, DECLS)).toEqual([expected]);
expect(validateVariables({ brandFont: {} }, DECLS)).toEqual([expected]);
});
it("flags a non-object non-string font value", () => {
expect(validateVariables({ brandFont: 42 }, DECLS)).toEqual([
{
kind: "type-mismatch",
variableId: "brandFont",
expected: "font (object {name, source} or string)",
actual: "number",
},
]);
});
it("accepts a valid image object and a fallback string", () => {
expect(validateVariables({ hero: { url: "https://x/y.png" } }, DECLS)).toEqual([]);
expect(validateVariables({ hero: "https://x/y.png" }, DECLS)).toEqual([]);
});
it("flags an image object missing a string url", () => {
expect(validateVariables({ hero: { foo: 42 } }, DECLS)).toEqual([
{
kind: "type-mismatch",
variableId: "hero",
expected: "image object {url: string}",
actual: "object missing string url",
},
]);
});
it("returns multiple issues at once", () => {
const issues = validateVariables({ title: 42, theme: "neon", extra: true }, DECLS);
expect(issues).toContainEqual({
kind: "type-mismatch",
variableId: "title",
expected: "string",
actual: "number",
});
expect(issues).toContainEqual({
kind: "enum-out-of-range",
variableId: "theme",
allowed: ["light", "dark"],
actual: "neon",
});
expect(issues).toContainEqual({ kind: "undeclared", variableId: "extra" });
});
});
describe("formatVariableValidationIssue", () => {
it("formats undeclared issues", () => {
expect(formatVariableValidationIssue({ kind: "undeclared", variableId: "extra" })).toBe(
'Variable "extra" is not declared in data-composition-variables.',
);
});
it("formats type-mismatch issues", () => {
expect(
formatVariableValidationIssue({
kind: "type-mismatch",
variableId: "count",
expected: "number",
actual: "string",
}),
).toBe('Variable "count" expected number, got string.');
});
it("formats enum-out-of-range issues", () => {
expect(
formatVariableValidationIssue({
kind: "enum-out-of-range",
variableId: "theme",
allowed: ["light", "dark"],
actual: "neon",
}),
).toBe('Variable "theme" must be one of "light", "dark" (got "neon").');
});
});