184 lines
4.9 KiB
TypeScript
184 lines
4.9 KiB
TypeScript
import { act, renderHook } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { useMermaidScale, useMermaidViewportWidth } from "./mermaid-block-hooks";
|
|
|
|
function setClientWidth(element: HTMLElement, width: number) {
|
|
Object.defineProperty(element, "clientWidth", { value: width, configurable: true });
|
|
}
|
|
|
|
function disableResizeObserver() {
|
|
Object.defineProperty(window, "ResizeObserver", {
|
|
configurable: true,
|
|
value: undefined,
|
|
writable: true,
|
|
});
|
|
}
|
|
|
|
class MockResizeObserver implements ResizeObserver {
|
|
private readonly callback: ResizeObserverCallback;
|
|
observed: Element | null = null;
|
|
|
|
constructor(callback: ResizeObserverCallback) {
|
|
this.callback = callback;
|
|
mockObservers.push(this);
|
|
}
|
|
|
|
observe(element: Element) {
|
|
this.observed = element;
|
|
}
|
|
|
|
disconnect() {
|
|
this.observed = null;
|
|
}
|
|
|
|
unobserve() {
|
|
this.observed = null;
|
|
}
|
|
|
|
trigger() {
|
|
this.callback([], this);
|
|
}
|
|
}
|
|
|
|
let mockObservers: MockResizeObserver[] = [];
|
|
|
|
function installMockResizeObserver() {
|
|
mockObservers = [];
|
|
window.ResizeObserver = MockResizeObserver;
|
|
return mockObservers;
|
|
}
|
|
|
|
describe("useMermaidViewportWidth", () => {
|
|
const originalResizeObserver = window.ResizeObserver;
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(window, "ResizeObserver", {
|
|
configurable: true,
|
|
value: originalResizeObserver,
|
|
writable: true,
|
|
});
|
|
});
|
|
|
|
it("measures the scroll region content width on mount", () => {
|
|
disableResizeObserver();
|
|
const el = document.createElement("div");
|
|
el.style.paddingLeft = "12px";
|
|
el.style.paddingRight = "8px";
|
|
setClientWidth(el, 300);
|
|
document.body.appendChild(el);
|
|
|
|
try {
|
|
const { result } = renderHook(() => useMermaidViewportWidth(el));
|
|
|
|
expect(result.current).toBe(280);
|
|
} finally {
|
|
el.remove();
|
|
}
|
|
});
|
|
|
|
it("uses the window resize fallback when ResizeObserver is unavailable", () => {
|
|
disableResizeObserver();
|
|
const el = document.createElement("div");
|
|
setClientWidth(el, 300);
|
|
document.body.appendChild(el);
|
|
const { result } = renderHook(() => useMermaidViewportWidth(el));
|
|
|
|
act(() => {
|
|
setClientWidth(el, 240);
|
|
window.dispatchEvent(new Event("resize"));
|
|
});
|
|
|
|
expect(result.current).toBe(240);
|
|
el.remove();
|
|
});
|
|
|
|
it("does not replace the last width while the scroll region is hidden", () => {
|
|
disableResizeObserver();
|
|
const el = document.createElement("div");
|
|
setClientWidth(el, 300);
|
|
document.body.appendChild(el);
|
|
const { result } = renderHook(() => useMermaidViewportWidth(el));
|
|
|
|
act(() => {
|
|
el.style.display = "none";
|
|
setClientWidth(el, 0);
|
|
window.dispatchEvent(new Event("resize"));
|
|
});
|
|
|
|
expect(result.current).toBe(300);
|
|
el.remove();
|
|
});
|
|
|
|
it("observes a replacement scroll region after the element changes", () => {
|
|
const observers = installMockResizeObserver();
|
|
|
|
const first = document.createElement("div");
|
|
const second = document.createElement("div");
|
|
setClientWidth(first, 200);
|
|
setClientWidth(second, 320);
|
|
document.body.append(first, second);
|
|
|
|
try {
|
|
const { result, rerender } = renderHook(({ element }) => useMermaidViewportWidth(element), {
|
|
initialProps: { element: first },
|
|
});
|
|
|
|
expect(result.current).toBe(200);
|
|
expect(observers).toHaveLength(1);
|
|
expect(observers[0].observed).toBe(first);
|
|
|
|
rerender({ element: second });
|
|
|
|
expect(result.current).toBe(320);
|
|
expect(observers).toHaveLength(2);
|
|
expect(observers[0].observed).toBeNull();
|
|
expect(observers[1].observed).toBe(second);
|
|
|
|
act(() => {
|
|
setClientWidth(second, 360);
|
|
observers[1].trigger();
|
|
});
|
|
|
|
expect(result.current).toBe(360);
|
|
} finally {
|
|
first.remove();
|
|
second.remove();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("useMermaidScale", () => {
|
|
it("preserves manual zoom across viewport changes and reset returns to the latest fit", () => {
|
|
const { result, rerender } = renderHook(
|
|
({ viewportWidth }) => useMermaidScale({ w: 1200, h: 400 }, viewportWidth),
|
|
{ initialProps: { viewportWidth: 600 } },
|
|
);
|
|
|
|
expect(result.current.scale).toBe(0.5);
|
|
|
|
act(() => result.current.zoomIn());
|
|
expect(result.current.scale).toBe(0.6);
|
|
|
|
rerender({ viewportWidth: 300 });
|
|
expect(result.current.scale).toBe(0.6);
|
|
|
|
act(() => result.current.zoomReset());
|
|
expect(result.current.scale).toBe(0.25);
|
|
});
|
|
|
|
it("resetAutoScale returns subsequent measurements to auto-fit behavior", () => {
|
|
const { result, rerender } = renderHook(
|
|
({ viewportWidth }) => useMermaidScale({ w: 1200, h: 400 }, viewportWidth),
|
|
{ initialProps: { viewportWidth: 600 } },
|
|
);
|
|
|
|
act(() => result.current.zoomOut());
|
|
expect(result.current.scale).toBe(0.4);
|
|
|
|
act(() => result.current.resetAutoScale());
|
|
rerender({ viewportWidth: 300 });
|
|
|
|
expect(result.current.scale).toBe(0.25);
|
|
});
|
|
});
|