Files
hyperframes/packages/studio/src/components/editor/PropertyPanel.test.ts
T
Miguel Ángel 037266e72b feat(studio): timeline revamp with active-clip highlighting and hide controls (#2017)
Timeline UI
- Highlight clips visible at the playhead in the primary color; others share one neutral color
- Minimalist rounded clips, single-color track rows, no gutter icons or superscript labels
- Per-track eye toggle and a per-element hide button in the design panel
- Ruler zoom fixes: sub-second tick intervals and correct label formatting at high zoom
- Sticky gutter so track controls stay visible while scrolling

WYSIWYG visibility (data-hidden)
- Runtime honors data-hidden (display:none), so hiding affects the render, not just the preview
- HTML stays the source of truth; hide state persists and round-trips on reload

Split several studio files to stay under the 600-line cap; pure relocations, no behavior change.
2026-07-07 04:26:56 -04:00

141 lines
5.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { TimelineElement } from "../../player";
import {
buildInsetClipPathSides,
buildStrokeStyleUpdates,
buildStrokeWidthStyleUpdates,
getClipPathInsetPx,
getCssFilterFunctionPx,
inferBoxShadowPreset,
inferClipPathPreset,
normalizePanelPxValue,
parseInsetClipPathSides,
setCssFilterFunctionPx,
} from "./PropertyPanel";
import { isSelectedElementHidden } from "./propertyPanelHelpers";
describe("PropertyPanel style helpers", () => {
it("normalizes bounded pixel values without accepting incompatible units", () => {
expect(normalizePanelPxValue("12", { min: 0, max: 40 })).toBe("12px");
expect(normalizePanelPxValue("12.50px", { min: 0, max: 40 })).toBe("12.5px");
expect(normalizePanelPxValue("-8", { min: 0, max: 40 })).toBe("0px");
expect(normalizePanelPxValue("80px", { min: 0, max: 40 })).toBe("40px");
expect(normalizePanelPxValue("1.2rem", { min: 0, max: 40 })).toBeNull();
expect(normalizePanelPxValue("auto", { min: 0, max: 40 })).toBeNull();
});
it("adds, replaces, and removes a named filter function while preserving other filters", () => {
expect(setCssFilterFunctionPx("none", "blur", 12)).toBe("blur(12px)");
expect(setCssFilterFunctionPx("brightness(1.08)", "blur", 4.5)).toBe(
"brightness(1.08) blur(4.5px)",
);
expect(setCssFilterFunctionPx("brightness(1.08) blur(12px) saturate(1.2)", "blur", 2)).toBe(
"brightness(1.08) saturate(1.2) blur(2px)",
);
expect(setCssFilterFunctionPx("brightness(1.08) blur(12px)", "blur", 0)).toBe(
"brightness(1.08)",
);
expect(setCssFilterFunctionPx("blur(12px)", "blur", 0)).toBe("none");
expect(getCssFilterFunctionPx("brightness(1.08) blur(3.5px)", "blur")).toBe(3.5);
expect(getCssFilterFunctionPx("drop-shadow(0 2px 8px black)", "blur")).toBe(0);
});
it("infers shadow and clip presets without losing custom values", () => {
expect(inferBoxShadowPreset("none")).toBe("none");
expect(inferBoxShadowPreset("0 12px 36px rgba(0, 0, 0, 0.28)")).toBe("soft");
expect(inferBoxShadowPreset("0 2px 4px red")).toBe("custom");
expect(inferClipPathPreset(undefined)).toBe("none");
expect(inferClipPathPreset("inset(12px round 8px)")).toBe("inset");
expect(inferClipPathPreset("circle(50% at 50% 50%)")).toBe("circle");
expect(inferClipPathPreset("polygon(0 0, 100% 0, 100% 100%)")).toBe("custom");
expect(getClipPathInsetPx("inset(12.5px round 8px)")).toBe(12.5);
expect(getClipPathInsetPx("circle(50% at 50% 50%)")).toBe(0);
});
it("builds and parses 4-side inset clip paths without losing radius", () => {
expect(buildInsetClipPathSides({ top: 10, right: 20, bottom: 30, left: 40 }, 6)).toBe(
"inset(10px 20px 30px 40px round 6px)",
);
expect(parseInsetClipPathSides("inset(10px 20px 30px 40px round 6px)")).toEqual({
top: 10,
right: 20,
bottom: 30,
left: 40,
radius: 6,
});
});
it("emits the single-value inset form when all sides are equal", () => {
expect(buildInsetClipPathSides({ top: 12.5, right: 12.5, bottom: 12.5, left: 12.5 })).toBe(
"inset(12.5px)",
);
expect(parseInsetClipPathSides("inset(12.5px)")).toEqual({
top: 12.5,
right: 12.5,
bottom: 12.5,
left: 12.5,
radius: 0,
});
expect(getClipPathInsetPx("inset(12.5px 12.5px 12.5px 12.5px)")).toBe(12.5);
});
it("accepts CSS shorthand inset values and rejects unsupported clip paths", () => {
expect(parseInsetClipPathSides("inset(10px 20px)")).toEqual({
top: 10,
right: 20,
bottom: 10,
left: 20,
radius: 0,
});
expect(parseInsetClipPathSides("inset(10px 20px 30px)")).toEqual({
top: 10,
right: 20,
bottom: 30,
left: 20,
radius: 0,
});
expect(parseInsetClipPathSides("inset(10%)")).toBeNull();
expect(parseInsetClipPathSides("circle(50% at 50% 50%)")).toBeNull();
});
it("keeps stroke width and style edits visually effective", () => {
expect(buildStrokeWidthStyleUpdates("3px", "none")).toEqual([
["border-width", "3px"],
["border-style", "solid"],
]);
expect(buildStrokeWidthStyleUpdates("0px", "none")).toEqual([["border-width", "0px"]]);
expect(buildStrokeWidthStyleUpdates("3px", "dashed")).toEqual([["border-width", "3px"]]);
expect(buildStrokeStyleUpdates("dashed", "0px")).toEqual([
["border-style", "dashed"],
["border-width", "1px"],
]);
expect(buildStrokeStyleUpdates("none", "4px")).toEqual([["border-style", "none"]]);
expect(buildStrokeStyleUpdates("solid", "4px")).toEqual([["border-style", "solid"]]);
});
});
describe("isSelectedElementHidden", () => {
it("reads hidden state by selected timeline id or key", () => {
const elements: TimelineElement[] = [
{ id: "visible", tag: "div", start: 0, duration: 1, track: 0 },
{ id: "hidden", tag: "div", start: 0, duration: 1, track: 0, hidden: true },
{
id: "keyed-hidden",
key: "scene.html:#keyed-hidden",
tag: "div",
start: 0,
duration: 1,
track: 0,
hidden: true,
},
];
expect(isSelectedElementHidden(elements, null)).toBe(false);
expect(isSelectedElementHidden(elements, "visible")).toBe(false);
expect(isSelectedElementHidden(elements, "hidden")).toBe(true);
expect(isSelectedElementHidden(elements, "scene.html:#keyed-hidden")).toBe(true);
});
});