Files
kandev/apps/web/components/shared/use-markdown-table-resize.test.ts
T

205 lines
6.2 KiB
TypeScript

import { act, cleanup, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useMarkdownTableResize } from "./use-markdown-table-resize";
let notifyMutation: MutationCallback;
const observeMutation = vi.fn();
class ResizeObserverStub {
disconnect = vi.fn();
observe = vi.fn();
}
class MutationObserverStub {
disconnect = vi.fn();
observe = observeMutation;
constructor(callback: MutationCallback) {
notifyMutation = callback;
}
}
function rect(left: number, top: number, width: number, height: number): DOMRect {
return {
bottom: top + height,
height,
left,
right: left + width,
top,
width,
x: left,
y: top,
toJSON: () => ({}),
};
}
function createTableGeometry(widths: number[]) {
let measuredWidths = [...widths];
const wrapper = document.createElement("div");
const table = document.createElement("table");
const row = table.insertRow();
wrapper.append(table);
document.body.append(wrapper);
Object.defineProperty(wrapper, "getBoundingClientRect", {
value: () => rect(10, 20, 300, 100),
});
Object.defineProperty(table, "getBoundingClientRect", {
value: () => rect(10, 20, 300, 100),
});
widths.forEach((_, index) => {
const cell = row.insertCell();
Object.defineProperty(cell, "getBoundingClientRect", {
value: () => {
const left = 10 + measuredWidths.slice(0, index).reduce((sum, width) => sum + width, 0);
return rect(left, 20, measuredWidths[index] ?? 0, 30);
},
});
});
return {
row,
setMeasuredWidths: (next: number[]) => {
measuredWidths = next;
},
table,
wrapper,
};
}
function renderResizeHook() {
const rendered = renderHook(({ enabled }) => useMarkdownTableResize(enabled), {
initialProps: { enabled: false },
});
const elements = createTableGeometry([120, 180]);
act(() => {
rendered.result.current.tableRef.current = elements.table;
rendered.result.current.wrapperRef.current = elements.wrapper;
});
rendered.rerender({ enabled: true });
return { ...rendered, ...elements };
}
function keyboardEvent(key: string) {
return { key, preventDefault: vi.fn() };
}
beforeEach(() => {
observeMutation.mockClear();
vi.stubGlobal("ResizeObserver", ResizeObserverStub);
vi.stubGlobal("MutationObserver", MutationObserverStub);
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
describe("useMarkdownTableResize", () => {
it("observes streamed text mutations that can rebalance automatic columns", () => {
const { table } = renderResizeHook();
expect(observeMutation).toHaveBeenCalledWith(table, {
characterData: true,
childList: true,
subtree: true,
});
});
it("resizes with arrow keys and resets with Enter", () => {
const { result } = renderResizeHook();
const right = keyboardEvent("ArrowRight");
const left = keyboardEvent("ArrowLeft");
const enter = keyboardEvent("Enter");
act(() => result.current.resizeWithKeyboard(0, right as never));
expect(right.preventDefault).toHaveBeenCalledOnce();
expect(result.current.columnWidths).toEqual([128, 172]);
expect(result.current.fixedTableWidth).toBe(300);
act(() => result.current.resizeWithKeyboard(0, left as never));
expect(left.preventDefault).toHaveBeenCalledOnce();
expect(result.current.columnWidths).toEqual([120, 180]);
act(() => result.current.resizeWithKeyboard(0, enter as never));
expect(enter.preventDefault).toHaveBeenCalledOnce();
expect(result.current.columnWidths).toBeNull();
expect(result.current.fixedTableWidth).toBeNull();
});
it("remeasures separator geometry when controlled widths change", () => {
const { result, setMeasuredWidths } = renderResizeHook();
expect(result.current.geometry?.boundaries[0]).toBe(120);
setMeasuredWidths([140, 160]);
act(() => result.current.resizeWithKeyboard(0, keyboardEvent("ArrowRight") as never));
expect(result.current.geometry?.boundaries[0]).toBe(140);
});
it("clears custom widths when resizing becomes disabled", () => {
const { result, rerender } = renderResizeHook();
act(() => result.current.resizeWithKeyboard(0, keyboardEvent("ArrowRight") as never));
expect(result.current.columnWidths).not.toBeNull();
rerender({ enabled: false });
expect(result.current.columnWidths).toBeNull();
expect(result.current.fixedTableWidth).toBeNull();
});
it("clears an active drag and document resize state when resizing becomes disabled", () => {
const { result, rerender } = renderResizeHook();
const pointer = {
button: 0,
clientX: 130,
currentTarget: { setPointerCapture: vi.fn() },
pointerId: 7,
preventDefault: vi.fn(),
};
act(() => result.current.startResize(0, pointer as never));
expect(result.current.activeBoundary).toBe(0);
expect(document.body.style.cursor).toBe("col-resize");
expect(document.body.style.userSelect).toBe("none");
rerender({ enabled: false });
expect(result.current.activeBoundary).toBeNull();
expect(document.body.style.cursor).toBe("");
expect(document.body.style.userSelect).toBe("");
});
it("keeps automatic layout when a pointer drag ends without movement", () => {
const { result } = renderResizeHook();
const target = {
hasPointerCapture: vi.fn(() => true),
releasePointerCapture: vi.fn(),
setPointerCapture: vi.fn(),
};
const pointer = {
button: 0,
clientX: 130,
currentTarget: target,
pointerId: 7,
preventDefault: vi.fn(),
};
act(() => result.current.startResize(0, pointer as never));
act(() => result.current.finishDrag(pointer as never, false));
expect(result.current.columnWidths).toBeNull();
expect(result.current.fixedTableWidth).toBeNull();
expect(target.releasePointerCapture).toHaveBeenCalledWith(7);
});
it("clears custom widths when the table column count changes", () => {
const { result, row } = renderResizeHook();
act(() => result.current.resizeWithKeyboard(0, keyboardEvent("ArrowRight") as never));
row.insertCell();
act(() => notifyMutation([], {} as MutationObserver));
expect(result.current.columnWidths).toBeNull();
expect(result.current.fixedTableWidth).toBeNull();
});
});