Skip to content
Open
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
15 changes: 15 additions & 0 deletions packages/core/src/compositionRoot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const AUTHORED_ROOT_ID_ATTR = "data-hf-authored-id";
export const INNER_ROOT_MARKER_ATTR = "data-hf-inner-root";

export const FLATTENED_INNER_ROOT_STRIP_ATTRS = [
"data-composition-id",
"data-composition-file",
"data-start",
"data-duration",
"data-end",
"data-track-index",
"data-track",
"data-composition-src",
"data-hf-authored-duration",
"data-hf-authored-end",
] as const;
2 changes: 1 addition & 1 deletion packages/studio/src/components/nle/NLELayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export const NLELayout = memo(function NLELayout({
>
{/* Preview + player controls */}
<div className="flex-1 min-h-0 flex flex-col">
<div className="flex-1 min-h-0 relative">
<div className="flex-1 min-h-0 relative" data-preview-pan-surface="true">
<NLEPreview
projectId={projectId}
iframeRef={iframeRef}
Expand Down
167 changes: 165 additions & 2 deletions packages/studio/src/components/nle/NLEPreview.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,101 @@
import { describe, expect, it } from "vitest";
import { getPreviewPlayerKey } from "./NLEPreview";
// @vitest-environment happy-dom

import React, { act, createRef } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { NLEPreview, getPreviewPlayerKey } from "./NLEPreview";

globalThis.IS_REACT_ACT_ENVIRONMENT = true;

vi.mock("../../player", async () => {
const React = await import("react");

return {
Player: React.forwardRef(function MockPlayer(
props: {
onLoad?: () => void;
style?: React.CSSProperties;
},
ref: React.ForwardedRef<HTMLIFrameElement>,
) {
React.useEffect(() => {
props.onLoad?.();
}, [props]);

return React.createElement("div", {
ref: ref as React.ForwardedRef<HTMLDivElement>,
"data-testid": "mock-player",
style: props.style,
});
}),
};
});

vi.mock("../../utils/studioUiPreferences", () => ({
readStudioUiPreferences: () => ({}),
writeStudioUiPreferences: () => {},
}));

class MockResizeObserver {
observe() {}
disconnect() {}
}

const originalResizeObserver = globalThis.ResizeObserver;

function setRect(node: Element, rect: { width: number; height: number }) {
Object.defineProperty(node, "getBoundingClientRect", {
configurable: true,
value: () => ({
x: 0,
y: 0,
left: 0,
top: 0,
right: rect.width,
bottom: rect.height,
width: rect.width,
height: rect.height,
toJSON: () => ({}),
}),
});
}

function renderPreview() {
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
const iframeRef = createRef<HTMLIFrameElement>();

act(() => {
root.render(
React.createElement(NLEPreview, {
projectId: "timeline-edit-playground",
iframeRef,
onIframeLoad: () => {},
}),
);
});

const viewport = host.querySelector('[aria-label="Composition preview"]') as HTMLDivElement;
const stage = host.querySelector('[data-testid="preview-zoom-stage"]') as HTMLDivElement;
expect(viewport).toBeTruthy();
expect(stage).toBeTruthy();

setRect(viewport, { width: 800, height: 600 });

return {
host,
root,
viewport,
stage,
cleanup() {
act(() => {
root.unmount();
});
host.remove();
},
};
}

describe("getPreviewPlayerKey", () => {
it("keeps the same player identity when only refreshKey changes", () => {
Expand Down Expand Up @@ -30,3 +126,70 @@ describe("getPreviewPlayerKey", () => {
);
});
});

describe("NLEPreview", () => {
beforeEach(() => {
globalThis.ResizeObserver = MockResizeObserver as typeof ResizeObserver;
});

afterEach(() => {
globalThis.ResizeObserver = originalResizeObserver;
});

it("pans the preview with middle mouse drag", () => {
const view = renderPreview();
const target = document.createElement("div");
view.stage.appendChild(target);

act(() => {
target.dispatchEvent(
new PointerEvent("pointerdown", {
bubbles: true,
pointerId: 1,
button: 1,
clientX: 240,
clientY: 180,
}),
);
document.dispatchEvent(
new PointerEvent("pointermove", {
bubbles: true,
pointerId: 1,
clientX: 300,
clientY: 220,
}),
);
document.dispatchEvent(
new PointerEvent("pointerup", {
bubbles: true,
pointerId: 1,
}),
);
});

expect(view.stage.style.transform).toContain("translate(48px, 40px)");
view.cleanup();
});

it("pans the preview with a two-finger wheel gesture", () => {
const view = renderPreview();
const target = document.createElement("div");
view.stage.appendChild(target);

act(() => {
target.dispatchEvent(
new WheelEvent("wheel", {
bubbles: true,
cancelable: true,
clientX: 240,
clientY: 180,
deltaX: -30,
deltaY: 24,
}),
);
});

expect(view.stage.style.transform).toContain("translate(30px, -24px)");
view.cleanup();
});
});
Loading
Loading