mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-13 20:58:33 +00:00
Merge branch 'fix/studio-sfx-u13' into fix/studio-ui-audit-fixes
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, createElement, type ReactNode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { TimelineElement } from "../../player";
|
||||
import type { DomEditSelection } from "./domEditing";
|
||||
import {
|
||||
PropertyPanel,
|
||||
buildInsetClipPathSides,
|
||||
buildStrokeStyleUpdates,
|
||||
buildStrokeWidthStyleUpdates,
|
||||
@@ -13,6 +19,186 @@ import {
|
||||
setCssFilterFunctionPx,
|
||||
} from "./PropertyPanel";
|
||||
import { isSelectedElementHidden } from "./propertyPanelHelpers";
|
||||
import { StyleSections } from "./propertyPanelStyleSections";
|
||||
|
||||
vi.mock("../../contexts/StudioContext", () => ({
|
||||
useStudioShellContext: () => ({ showToast: () => undefined }),
|
||||
}));
|
||||
|
||||
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
||||
|
||||
function makeSelection(
|
||||
capabilityOverrides: Partial<DomEditSelection["capabilities"]>,
|
||||
): DomEditSelection {
|
||||
return {
|
||||
element: document.createElement("div"),
|
||||
id: "selected",
|
||||
selector: "#selected",
|
||||
selectorIndex: 0,
|
||||
label: "Selected element",
|
||||
tagName: "div",
|
||||
sourceFile: "index.html",
|
||||
compositionPath: "index.html",
|
||||
isCompositionHost: false,
|
||||
isInsideLockedComposition: false,
|
||||
boundingBox: { x: 0, y: 0, width: 100, height: 100 },
|
||||
textContent: null,
|
||||
dataAttributes: {},
|
||||
inlineStyles: {},
|
||||
computedStyles: {},
|
||||
textFields: [],
|
||||
capabilities: {
|
||||
canSelect: true,
|
||||
canEditStyles: true,
|
||||
canCrop: true,
|
||||
canMove: true,
|
||||
canResize: true,
|
||||
canApplyManualOffset: true,
|
||||
canApplyManualSize: true,
|
||||
canApplyManualRotation: true,
|
||||
...capabilityOverrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function renderInDocument(node: ReactNode): {
|
||||
host: HTMLDivElement;
|
||||
unmount: () => void;
|
||||
} {
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
act(() => {
|
||||
root.render(node);
|
||||
});
|
||||
return {
|
||||
host,
|
||||
unmount: () => {
|
||||
act(() => root.unmount());
|
||||
host.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function renderPropertyPanel(element: DomEditSelection) {
|
||||
return renderInDocument(
|
||||
createElement(PropertyPanel, {
|
||||
projectId: "project",
|
||||
projectDir: null,
|
||||
assets: [],
|
||||
element,
|
||||
onClearSelection: vi.fn(),
|
||||
onSetStyle: vi.fn(),
|
||||
onSetAttribute: vi.fn(),
|
||||
onSetAttributeLive: vi.fn(),
|
||||
onSetHtmlAttribute: vi.fn(),
|
||||
onSetManualOffset: vi.fn(),
|
||||
onSetManualSize: vi.fn(),
|
||||
onSetManualRotation: vi.fn(),
|
||||
onSetText: vi.fn(),
|
||||
onSetTextFieldStyle: vi.fn(),
|
||||
onAddTextField: vi.fn(),
|
||||
onRemoveTextField: vi.fn(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function renderStyleSections(element: DomEditSelection) {
|
||||
return renderInDocument(
|
||||
createElement(StyleSections, {
|
||||
projectId: "project",
|
||||
element,
|
||||
styles: {},
|
||||
assets: [],
|
||||
onSetStyle: vi.fn(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
describe("PropertyPanel disabled control reasons", () => {
|
||||
it("renders the capability reason on a disabled Layout section", () => {
|
||||
const reason = "This element belongs to a locked composition.";
|
||||
const panel = renderPropertyPanel(
|
||||
makeSelection({
|
||||
canEditStyles: false,
|
||||
canApplyManualOffset: false,
|
||||
canApplyManualSize: false,
|
||||
canApplyManualRotation: false,
|
||||
reasonIfDisabled: reason,
|
||||
}),
|
||||
);
|
||||
|
||||
const layout = panel.host.querySelector('[data-panel-section="layout"]');
|
||||
expect(layout?.querySelector("[data-disabled-reason]")?.textContent).toBe(reason);
|
||||
panel.unmount();
|
||||
});
|
||||
|
||||
it("renders no disabled reason note for an enabled selection", () => {
|
||||
const panel = renderPropertyPanel(makeSelection({}));
|
||||
|
||||
expect(panel.host.querySelector('[data-panel-section="layout"] [data-disabled-reason]')).toBe(
|
||||
null,
|
||||
);
|
||||
panel.unmount();
|
||||
});
|
||||
|
||||
it("renders no empty disabled reason chrome when no reason is available", () => {
|
||||
const panel = renderPropertyPanel(
|
||||
makeSelection({
|
||||
canEditStyles: false,
|
||||
canApplyManualOffset: false,
|
||||
canApplyManualSize: false,
|
||||
canApplyManualRotation: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(panel.host.querySelector('[data-panel-section="layout"] [data-disabled-reason]')).toBe(
|
||||
null,
|
||||
);
|
||||
panel.unmount();
|
||||
});
|
||||
|
||||
it("renders the capability reason on a disabled Fill section", () => {
|
||||
const reason = "This element belongs to a locked composition.";
|
||||
const panel = renderStyleSections(
|
||||
makeSelection({
|
||||
canEditStyles: false,
|
||||
reasonIfDisabled: reason,
|
||||
}),
|
||||
);
|
||||
|
||||
const fill = panel.host.querySelector('[data-panel-section="fill"]');
|
||||
expect(fill?.querySelector("[data-disabled-reason]")?.textContent).toBe(reason);
|
||||
panel.unmount();
|
||||
});
|
||||
|
||||
it("renders no stale disabled reason note on an enabled Fill section", () => {
|
||||
const panel = renderStyleSections(
|
||||
makeSelection({
|
||||
canEditStyles: true,
|
||||
reasonIfDisabled: "This reason is not relevant while style editing is enabled.",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(panel.host.querySelector('[data-panel-section="fill"] [data-disabled-reason]')).toBe(
|
||||
null,
|
||||
);
|
||||
panel.unmount();
|
||||
});
|
||||
|
||||
it("renders no empty disabled reason chrome on Fill when no reason is available", () => {
|
||||
const panel = renderStyleSections(
|
||||
makeSelection({
|
||||
canEditStyles: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(panel.host.querySelector('[data-panel-section="fill"] [data-disabled-reason]')).toBe(
|
||||
null,
|
||||
);
|
||||
panel.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe("PropertyPanel style helpers", () => {
|
||||
it("normalizes bounded pixel values without accepting incompatible units", () => {
|
||||
|
||||
@@ -378,7 +378,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
/>
|
||||
)}
|
||||
|
||||
<Section title="Layout">
|
||||
<Section title="Layout" disabledReason={element.capabilities.reasonIfDisabled}>
|
||||
<div className={RESPONSIVE_GRID}>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex-1">
|
||||
|
||||
@@ -360,11 +360,13 @@ export function Section({
|
||||
title,
|
||||
children,
|
||||
accessory,
|
||||
disabledReason,
|
||||
defaultCollapsed = false,
|
||||
}: {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
accessory?: ReactNode;
|
||||
disabledReason?: string;
|
||||
defaultCollapsed?: boolean;
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState(defaultCollapsed);
|
||||
@@ -406,7 +408,19 @@ export function Section({
|
||||
</button>
|
||||
{accessory && <div className="flex flex-shrink-0 items-center">{accessory}</div>}
|
||||
</div>
|
||||
{!collapsed && <div className="px-4 pb-3">{children}</div>}
|
||||
{!collapsed && (
|
||||
<div className="px-4 pb-3">
|
||||
{disabledReason && (
|
||||
<p
|
||||
data-disabled-reason
|
||||
className="mb-2 rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-amber-400"
|
||||
>
|
||||
{disabledReason}
|
||||
</p>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -145,7 +145,11 @@ export function StyleSections({
|
||||
return (
|
||||
<>
|
||||
{isFlex && (
|
||||
<Section title="Flex" defaultCollapsed>
|
||||
<Section
|
||||
title="Flex"
|
||||
disabledReason={styleEditingDisabled ? element.capabilities.reasonIfDisabled : undefined}
|
||||
defaultCollapsed
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<SegmentedControl
|
||||
disabled={styleEditingDisabled}
|
||||
@@ -190,7 +194,11 @@ export function StyleSections({
|
||||
)}
|
||||
|
||||
{hasVisualBackground && (
|
||||
<Section title="Radius" defaultCollapsed>
|
||||
<Section
|
||||
title="Radius"
|
||||
disabledReason={styleEditingDisabled ? element.capabilities.reasonIfDisabled : undefined}
|
||||
defaultCollapsed
|
||||
>
|
||||
<BorderRadiusEditor
|
||||
tl={radiusTL}
|
||||
tr={radiusTR}
|
||||
@@ -215,7 +223,11 @@ export function StyleSections({
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section title="Stroke" defaultCollapsed>
|
||||
<Section
|
||||
title="Stroke"
|
||||
disabledReason={styleEditingDisabled ? element.capabilities.reasonIfDisabled : undefined}
|
||||
defaultCollapsed
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className={RESPONSIVE_GRID}>
|
||||
<MetricField
|
||||
@@ -262,7 +274,11 @@ export function StyleSections({
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Effects" defaultCollapsed>
|
||||
<Section
|
||||
title="Effects"
|
||||
disabledReason={styleEditingDisabled ? element.capabilities.reasonIfDisabled : undefined}
|
||||
defaultCollapsed
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<SelectField
|
||||
label="Shadow"
|
||||
@@ -315,7 +331,11 @@ export function StyleSections({
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Clip" defaultCollapsed>
|
||||
<Section
|
||||
title="Clip"
|
||||
disabledReason={styleEditingDisabled ? element.capabilities.reasonIfDisabled : undefined}
|
||||
defaultCollapsed
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className={RESPONSIVE_GRID}>
|
||||
<SelectField
|
||||
@@ -391,7 +411,11 @@ export function StyleSections({
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Transparency" defaultCollapsed>
|
||||
<Section
|
||||
title="Transparency"
|
||||
disabledReason={styleEditingDisabled ? element.capabilities.reasonIfDisabled : undefined}
|
||||
defaultCollapsed
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<SliderControl
|
||||
value={opacityValue}
|
||||
@@ -413,7 +437,10 @@ export function StyleSections({
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Fill">
|
||||
<Section
|
||||
title="Fill"
|
||||
disabledReason={styleEditingDisabled ? element.capabilities.reasonIfDisabled : undefined}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<SegmentedControl
|
||||
disabled={styleEditingDisabled}
|
||||
|
||||
Reference in New Issue
Block a user