* fix(app): require long-press for sidebar dnd on mobile * fix(app): right-edge open gesture for explorer sidebar * fix(app): left-edge open gesture for workspace sidebar * fix(app): allow gesture children to scroll on web * fix(app): lock viewport zoom on compact web * fix(app): don't submit on enter in compact web composer --------- Co-authored-by: jon <nikuscs@gmail.com>
168 lines
4.1 KiB
TypeScript
168 lines
4.1 KiB
TypeScript
/**
|
|
* @vitest-environment jsdom
|
|
*/
|
|
import React from "react";
|
|
import { act } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { DraggableList } from "./draggable-list.web";
|
|
|
|
interface DndContextProps {
|
|
onDragStart?: (event: { active: { id: string } }) => void;
|
|
onDragCancel?: () => void;
|
|
}
|
|
|
|
let latestDndContextProps: DndContextProps | null = null;
|
|
const dndKitMocks = vi.hoisted(() => ({
|
|
useSensor: vi.fn(() => ({})),
|
|
}));
|
|
|
|
vi.mock("@dnd-kit/core", () => ({
|
|
DndContext: ({ children, ...props }: React.PropsWithChildren<DndContextProps>) => {
|
|
latestDndContextProps = props;
|
|
return <div>{children}</div>;
|
|
},
|
|
closestCenter: vi.fn(),
|
|
KeyboardSensor: vi.fn(),
|
|
PointerSensor: vi.fn(),
|
|
useSensor: dndKitMocks.useSensor,
|
|
useSensors: vi.fn(() => []),
|
|
}));
|
|
|
|
vi.mock("@dnd-kit/sortable", () => ({
|
|
SortableContext: ({ children }: React.PropsWithChildren) => children,
|
|
arrayMove: <T,>(items: T[], from: number, to: number) => {
|
|
const next = [...items];
|
|
const [item] = next.splice(from, 1);
|
|
if (item !== undefined) {
|
|
next.splice(to, 0, item);
|
|
}
|
|
return next;
|
|
},
|
|
sortableKeyboardCoordinates: vi.fn(),
|
|
useSortable: () => ({
|
|
attributes: {},
|
|
listeners: {},
|
|
setNodeRef: vi.fn(),
|
|
setActivatorNodeRef: vi.fn(),
|
|
transform: null,
|
|
transition: undefined,
|
|
isDragging: false,
|
|
}),
|
|
verticalListSortingStrategy: {},
|
|
}));
|
|
|
|
vi.mock("./use-web-scrollbar", () => ({
|
|
useWebScrollViewScrollbar: () => ({
|
|
onLayout: vi.fn(),
|
|
onContentSizeChange: vi.fn(),
|
|
onScroll: vi.fn(),
|
|
overlay: null,
|
|
}),
|
|
}));
|
|
|
|
let root: Root | null = null;
|
|
let container: HTMLElement | null = null;
|
|
|
|
beforeEach(() => {
|
|
vi.stubGlobal("React", React);
|
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
vi.stubGlobal(
|
|
"ResizeObserver",
|
|
class ResizeObserver {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
},
|
|
);
|
|
latestDndContextProps = null;
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (root) {
|
|
act(() => {
|
|
root?.unmount();
|
|
});
|
|
}
|
|
root = null;
|
|
container?.remove();
|
|
container = null;
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
const DATA: string[] = ["alpha", "beta"];
|
|
|
|
function keyExtractor(item: string): string {
|
|
return item;
|
|
}
|
|
|
|
function renderItem({ item, isActive }: { item: string; isActive: boolean }) {
|
|
return (
|
|
<div data-active={String(isActive)} data-testid={`item-${item}`}>
|
|
{item}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function renderList({ useDragHandle = false }: { useDragHandle?: boolean } = {}): void {
|
|
act(() => {
|
|
root?.render(
|
|
<DraggableList
|
|
data={DATA}
|
|
keyExtractor={keyExtractor}
|
|
onDragEnd={vi.fn()}
|
|
renderItem={renderItem}
|
|
scrollEnabled={false}
|
|
useDragHandle={useDragHandle}
|
|
/>,
|
|
);
|
|
});
|
|
}
|
|
|
|
function getItemActiveState(item: string): string | null {
|
|
return (
|
|
container?.querySelector(`[data-testid="item-${item}"]`)?.getAttribute("data-active") ?? null
|
|
);
|
|
}
|
|
|
|
describe("DraggableList web", () => {
|
|
it("uses distance activation for default draggable rows", () => {
|
|
renderList();
|
|
|
|
expect(dndKitMocks.useSensor).toHaveBeenCalledWith(
|
|
expect.any(Function),
|
|
expect.objectContaining({
|
|
activationConstraint: { distance: 6 },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("requires a held pointer before activating handle-based drags", () => {
|
|
renderList({ useDragHandle: true });
|
|
|
|
expect(dndKitMocks.useSensor).toHaveBeenCalledWith(
|
|
expect.any(Function),
|
|
expect.objectContaining({
|
|
activationConstraint: { delay: 250, tolerance: 8 },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("clears active drag state when a drag is cancelled", () => {
|
|
renderList();
|
|
|
|
act(() => {
|
|
latestDndContextProps?.onDragStart?.({ active: { id: "alpha" } });
|
|
});
|
|
expect(getItemActiveState("alpha")).toBe("true");
|
|
|
|
act(() => {
|
|
latestDndContextProps?.onDragCancel?.();
|
|
});
|
|
expect(getItemActiveState("alpha")).toBe("false");
|
|
});
|
|
});
|