补充预览拖动选择回归测试
覆盖节点点击延迟到 pointerup 的选择时机 覆盖拖动嵌套节点缩放取消失焦和区域外松开 覆盖直接拖动节点不再由 click 单独选中
This commit is contained in:
@@ -62,6 +62,7 @@ function renderTree(
|
||||
options: {
|
||||
showFrame?: boolean;
|
||||
selectedNodeId?: string | null;
|
||||
onSelectNode?: ReturnType<typeof vi.fn>;
|
||||
onNodeContextMenu?: ReturnType<typeof vi.fn>;
|
||||
} = {},
|
||||
) {
|
||||
@@ -73,7 +74,7 @@ function renderTree(
|
||||
hiddenNodeIds={hiddenNodeIds}
|
||||
selectedNodeId={options.selectedNodeId ?? null}
|
||||
resources={resources}
|
||||
onSelectNode={vi.fn()}
|
||||
onSelectNode={options.onSelectNode ?? vi.fn()}
|
||||
onNodeContextMenu={options.onNodeContextMenu ?? vi.fn()}
|
||||
onNodePointerDown={vi.fn()}
|
||||
onNodePointerMove={vi.fn()}
|
||||
@@ -87,6 +88,15 @@ function renderTree(
|
||||
}
|
||||
|
||||
describe('UI tree preview visibility', () => {
|
||||
it('does not use click alone to select directly draggable nodes', () => {
|
||||
const onSelectNode = vi.fn();
|
||||
const rendered = renderTree('editor-overlay', new Set(), { onSelectNode });
|
||||
fireEvent.click(
|
||||
rendered.container.querySelector('[data-node-id="child"]')!,
|
||||
);
|
||||
expect(onSelectNode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shares node menu restrictions, disabled actions, and dismissal behavior', () => {
|
||||
const onClose = vi.fn();
|
||||
const onInsertChild = vi.fn();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { MutableRefObject } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { Node as UiNode } from '../src/features/ui-editor/types/Node';
|
||||
@@ -59,12 +60,14 @@ function pointerEvent(
|
||||
pointerId: number,
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
eventTarget: EventTarget = target,
|
||||
) {
|
||||
return {
|
||||
button: 0,
|
||||
clientX,
|
||||
clientY,
|
||||
currentTarget: target,
|
||||
target: eventTarget,
|
||||
pointerId,
|
||||
preventDefault: vi.fn(),
|
||||
shiftKey: false,
|
||||
@@ -83,10 +86,12 @@ function renderInteraction({
|
||||
activeImageId = 'page',
|
||||
scale = 1,
|
||||
tree = pageTree,
|
||||
previewRef,
|
||||
}: {
|
||||
activeImageId?: string | null;
|
||||
scale?: number;
|
||||
tree?: UITree | null;
|
||||
previewRef?: MutableRefObject<HTMLElement | null>;
|
||||
} = {}) {
|
||||
const canvasProjection = canvas();
|
||||
const viewportRef = { current: { scale } };
|
||||
@@ -98,6 +103,7 @@ function renderInteraction({
|
||||
logicalSize: { width: 320, height: 180 },
|
||||
spaceHeld: false,
|
||||
tree: currentTree,
|
||||
previewRef,
|
||||
viewportRef,
|
||||
}),
|
||||
{ initialProps: { imageId: activeImageId, currentTree: tree } },
|
||||
@@ -106,6 +112,119 @@ function renderInteraction({
|
||||
}
|
||||
|
||||
describe('useNodeTransformInteraction', () => {
|
||||
it('defers node selection until a clean pointerup', () => {
|
||||
const { result, canvasProjection } = renderInteraction();
|
||||
const target = gestureTarget();
|
||||
|
||||
act(() => {
|
||||
result.current.onNodePointerDown(pointerEvent(target, 1, 0, 0), child);
|
||||
});
|
||||
expect(canvasProjection.selectNode).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
result.current.onNodePointerUp(pointerEvent(target, 1, 1, 1));
|
||||
});
|
||||
expect(canvasProjection.selectNode).toHaveBeenCalledWith('child');
|
||||
});
|
||||
|
||||
it('does not select after a drag or pointer cancellation', () => {
|
||||
const { result, canvasProjection } = renderInteraction();
|
||||
const target = gestureTarget();
|
||||
|
||||
act(() => {
|
||||
result.current.onNodePointerDown(pointerEvent(target, 1, 0, 0), child);
|
||||
result.current.onNodePointerMove(pointerEvent(target, 1, 3, 0));
|
||||
result.current.onNodePointerUp(pointerEvent(target, 1, 3, 0));
|
||||
});
|
||||
expect(canvasProjection.selectNode).not.toHaveBeenCalled();
|
||||
expect(result.current.consumeNodeClick()).toBe(true);
|
||||
expect(result.current.consumeNodeClick()).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.onNodePointerDown(pointerEvent(target, 2, 0, 0), child);
|
||||
result.current.onNodePointerCancel(pointerEvent(target, 2, 0, 0));
|
||||
});
|
||||
expect(canvasProjection.selectNode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not select when a clean pointerup lands outside the preview', () => {
|
||||
const preview = document.createElement('div');
|
||||
vi.spyOn(preview, 'getBoundingClientRect').mockReturnValue({
|
||||
bottom: 10,
|
||||
height: 10,
|
||||
left: 0,
|
||||
right: 10,
|
||||
top: 0,
|
||||
width: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({}),
|
||||
});
|
||||
const previewRef = { current: preview };
|
||||
const { result, canvasProjection } = renderInteraction({ previewRef });
|
||||
const target = gestureTarget();
|
||||
|
||||
act(() => {
|
||||
result.current.onNodePointerDown(pointerEvent(target, 1, 1, 1), child);
|
||||
result.current.onNodePointerUp(pointerEvent(target, 1, 20, 20));
|
||||
});
|
||||
|
||||
expect(canvasProjection.selectNode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cancels the active gesture when the window loses focus', () => {
|
||||
const { result, canvasProjection } = renderInteraction();
|
||||
const target = gestureTarget();
|
||||
|
||||
act(() => {
|
||||
result.current.onNodePointerDown(pointerEvent(target, 1, 0, 0), child);
|
||||
window.dispatchEvent(new Event('blur'));
|
||||
result.current.onNodePointerUp(pointerEvent(target, 1, 0, 0));
|
||||
});
|
||||
|
||||
expect(canvasProjection.selectNode).not.toHaveBeenCalled();
|
||||
expect(target.releasePointerCapture).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('leaves nested child clicks to the child when the outer gesture did not move', () => {
|
||||
const { result, canvasProjection } = renderInteraction();
|
||||
const target = gestureTarget();
|
||||
const nestedTarget = document.createElement('span');
|
||||
nestedTarget.dataset.nodeId = 'nested';
|
||||
target.appendChild(nestedTarget);
|
||||
|
||||
act(() => {
|
||||
result.current.onNodePointerDown(
|
||||
pointerEvent(target, 1, 0, 0, nestedTarget),
|
||||
child,
|
||||
);
|
||||
result.current.onNodePointerUp(
|
||||
pointerEvent(target, 1, 0, 0, nestedTarget),
|
||||
);
|
||||
});
|
||||
|
||||
expect(canvasProjection.selectNode).toHaveBeenCalledWith('nested');
|
||||
});
|
||||
|
||||
it('defers resize selection until a clean pointerup as well', () => {
|
||||
const { result, canvasProjection } = renderInteraction();
|
||||
const target = gestureTarget();
|
||||
|
||||
act(() => {
|
||||
result.current.onNodeResizePointerDown(
|
||||
pointerEvent(target, 1, 0, 0),
|
||||
child,
|
||||
'se',
|
||||
);
|
||||
});
|
||||
expect(canvasProjection.selectNode).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
result.current.onNodeResizePointerUp(pointerEvent(target, 1, 1, 1));
|
||||
});
|
||||
expect(canvasProjection.selectNode).toHaveBeenCalledWith('child');
|
||||
});
|
||||
|
||||
it('makes drag and resize mutually exclusive, then permits the next gesture', () => {
|
||||
const { result, canvasProjection } = renderInteraction();
|
||||
const dragTarget = gestureTarget();
|
||||
|
||||
Reference in New Issue
Block a user