mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
fix(studio): make canvas selection hit intended elements (#1907)
* test(studio): add design-panel QA fixture and triage matrix Fixture project covering all panel-editable element archetypes, plus the QA findings matrix from the design-panel bug campaign. * fix(studio): make canvas selection hit intended elements - honor author pointer-events:none in hit-testing (was selecting invisible overlays) - pause playback before mousedown sampling; fall back to hover selection on null resolve - invalidate committed selection when the active composition changes - double-click keeps selection and defers to multi-candidate click cycling * fix(studio): close remaining selection-layer review findings - hoverSelection fallback now wired at all 3 mousedown call sites (box-click, blocked-drag, plain overlay click) instead of just the overlay path - pointer-events override detection reads computed style, not inline style, so a CSS-class opt-in (not just inline style=) on a descendant is honored - defensively remove the pointer-events override before the group-fallback check too, closing a theoretical gap in the no-elementsFromPoint branch - a click that resolves to nothing (dead-zone / deselect) no longer leaves playback paused if it was already playing
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { installReactActEnvironment, makeSelection } from "./domSelectionTestHarness";
|
||||
import { useDomSelection } from "./useDomSelection";
|
||||
|
||||
installReactActEnvironment();
|
||||
|
||||
interface HarnessProps {
|
||||
activeCompPath: string | null;
|
||||
projectId: string | null;
|
||||
refreshKey: number;
|
||||
}
|
||||
|
||||
function renderHarness(initialProps: HarnessProps): {
|
||||
current: () => ReturnType<typeof useDomSelection>;
|
||||
rerender: (props: HarnessProps) => void;
|
||||
cleanup: () => void;
|
||||
} {
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
let currentHook: ReturnType<typeof useDomSelection> | null = null;
|
||||
|
||||
function Harness(props: HarnessProps) {
|
||||
currentHook = useDomSelection({
|
||||
projectId: props.projectId,
|
||||
activeCompPath: props.activeCompPath,
|
||||
isMasterView: false,
|
||||
compIdToSrc: new Map(),
|
||||
captionEditMode: false,
|
||||
previewIframeRef: { current: null },
|
||||
timelineElements: [],
|
||||
setSelectedTimelineElementId: vi.fn(),
|
||||
setRightCollapsed: vi.fn(),
|
||||
setRightPanelTab: vi.fn(),
|
||||
previewIframe: null,
|
||||
refreshKey: props.refreshKey,
|
||||
rightPanelTab: "design",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const rerender = (props: HarnessProps) => {
|
||||
act(() => {
|
||||
root.render(React.createElement(Harness, props));
|
||||
});
|
||||
};
|
||||
|
||||
rerender(initialProps);
|
||||
|
||||
return {
|
||||
current: () => {
|
||||
if (!currentHook) throw new Error("Expected hook result");
|
||||
return currentHook;
|
||||
},
|
||||
rerender,
|
||||
cleanup: () => {
|
||||
act(() => root.unmount());
|
||||
host.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setupSelectedHarness() {
|
||||
const element = document.createElement("div");
|
||||
element.id = "headline";
|
||||
const selection = makeSelection("Headline", element);
|
||||
const harness = renderHarness({
|
||||
activeCompPath: "intro.html",
|
||||
projectId: "project-1",
|
||||
refreshKey: 0,
|
||||
});
|
||||
act(() => harness.current().applyDomSelection(selection));
|
||||
return { selection, harness };
|
||||
}
|
||||
|
||||
describe("useDomSelection", () => {
|
||||
it("clears a committed selection when the active composition path changes", () => {
|
||||
const { selection, harness } = setupSelectedHarness();
|
||||
expect(harness.current().domEditSelection).toBe(selection);
|
||||
|
||||
harness.rerender({
|
||||
activeCompPath: "outro.html",
|
||||
projectId: "project-1",
|
||||
refreshKey: 0,
|
||||
});
|
||||
|
||||
expect(harness.current().domEditSelection).toBe(null);
|
||||
harness.cleanup();
|
||||
});
|
||||
|
||||
it("preserves a committed selection when composition identity is unchanged", () => {
|
||||
const { selection, harness } = setupSelectedHarness();
|
||||
harness.rerender({
|
||||
activeCompPath: "intro.html",
|
||||
projectId: "project-1",
|
||||
refreshKey: 1,
|
||||
});
|
||||
|
||||
expect(harness.current().domEditSelection).toBe(selection);
|
||||
harness.cleanup();
|
||||
});
|
||||
|
||||
it("preserves selection when clearing an already inactive group scope", () => {
|
||||
const { selection, harness } = setupSelectedHarness();
|
||||
act(() => harness.current().setActiveGroupElement(null));
|
||||
|
||||
expect(harness.current().domEditSelection).toBe(selection);
|
||||
harness.cleanup();
|
||||
});
|
||||
|
||||
it("preserves selection when entering the already active group scope", () => {
|
||||
const group = document.createElement("div");
|
||||
const child = document.createElement("span");
|
||||
child.id = "headline";
|
||||
group.append(child);
|
||||
const selection = makeSelection("Headline", child);
|
||||
const harness = renderHarness({
|
||||
activeCompPath: "intro.html",
|
||||
projectId: "project-1",
|
||||
refreshKey: 0,
|
||||
});
|
||||
|
||||
act(() => harness.current().setActiveGroupElement(group));
|
||||
act(() => harness.current().applyDomSelection(selection));
|
||||
act(() => harness.current().setActiveGroupElement(group));
|
||||
|
||||
expect(harness.current().domEditSelection).toBe(selection);
|
||||
harness.cleanup();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user