Files
Genarrative/packages/image-canvas-react/src/sharedCanvasReact.test.tsx
T
k88936 cfbc6c36a5
Project CI / Frontend tests (pull_request) Failing after 2m16s
Project CI / Repository checks (pull_request) Successful in 2m45s
Project CI / Native shell tests (pull_request) Failing after 4m47s
Project CI / Backend tests (pull_request) Successful in 5m58s
共享 CanvasWorld 统一超采样
默认以 2 倍内容分辨率渲染并保持 viewport.scale 的最终视觉比例

为 world 内 SVG 几何元素启用非缩放描边

补充超采样 transform 与 inverse-scale 契约测试
2026-09-05 11:14:56 +08:00

166 lines
5.1 KiB
TypeScript

/* @vitest-environment jsdom */
import type { CanvasLayer } from '@genarrative/image-canvas-core';
import { act, render, renderHook, screen } from '@testing-library/react';
import { createRef } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { CanvasWorld } from './CanvasWorld';
import { useImageCanvasViewportControls } from './useImageCanvasViewportControls';
function layer(id: string, x: number): CanvasLayer {
return {
id,
resourceId: `resource-${id}`,
title: id,
src: `media:${id}`,
x,
y: 0,
width: 100,
height: 100,
originalWidth: 100,
originalHeight: 100,
zIndex: 1,
sourceType: 'uploaded',
};
}
function createViewport() {
const viewport = document.createElement('div');
Object.defineProperties(viewport, {
clientWidth: { configurable: true, value: 800 },
clientHeight: { configurable: true, value: 600 },
});
viewport.getBoundingClientRect = () => ({
left: 0,
top: 0,
right: 800,
bottom: 600,
width: 800,
height: 600,
x: 0,
y: 0,
toJSON: () => undefined,
});
document.body.append(viewport);
return viewport;
}
afterEach(() => {
document.body.replaceChildren();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
describe('shared canvas React lifecycle', () => {
it('renders a supersampled world while preserving the public viewport scale', () => {
render(
<CanvasWorld viewport={{ x: 12, y: 24, scale: 2 }}>
<span>content</span>
</CanvasWorld>,
);
const world = screen.getByText('content').parentElement?.parentElement;
const content = screen.getByText('content').parentElement;
expect(world?.style.width).toBe('12000px');
expect(world?.style.height).toBe('12000px');
expect(world?.style.transform).toBe('translate(12px, 24px) scale(1)');
expect(
world?.style.getPropertyValue('--genarrative-image-canvas-inverse-scale'),
).toBe('0.5');
expect(content?.style.zoom).toBe('2');
});
it('removes every native listener across repeated mounts', () => {
const viewport = createViewport();
const addListener = vi.spyOn(viewport, 'addEventListener');
const removeListener = vi.spyOn(viewport, 'removeEventListener');
const ref = createRef<HTMLDivElement>();
ref.current = viewport;
const options = {
canvasViewportRef: ref,
layers: [layer('a', 0)],
captureCanvasHistory: vi.fn(),
};
const first = renderHook(() => useImageCanvasViewportControls(options));
first.unmount();
const second = renderHook(() => useImageCanvasViewportControls(options));
second.unmount();
expect(
addListener.mock.calls.filter(([type]) => type === 'wheel'),
).toHaveLength(2);
expect(
removeListener.mock.calls.filter(([type]) => type === 'wheel'),
).toHaveLength(2);
});
it('disconnects the instance ResizeObserver on unmount', () => {
const viewport = createViewport();
const disconnect = vi.fn();
const observe = vi.fn();
vi.stubGlobal(
'ResizeObserver',
class {
observe = observe;
disconnect = disconnect;
},
);
const ref = createRef<HTMLDivElement>();
ref.current = viewport;
const hook = renderHook(() =>
useImageCanvasViewportControls({
canvasViewportRef: ref,
layers: [layer('a', 0)],
captureCanvasHistory: vi.fn(),
}),
);
expect(observe).toHaveBeenCalledWith(viewport);
hook.unmount();
expect(disconnect).toHaveBeenCalledTimes(1);
});
it('uses the minimap inside each viewport instead of another canvas instance', () => {
const firstViewport = createViewport();
const secondViewport = createViewport();
const firstMinimap = document.createElement('button');
const secondMinimap = document.createElement('button');
firstMinimap.className = 'genarrative-image-canvas__minimap';
secondMinimap.className = 'genarrative-image-canvas__minimap';
firstMinimap.getBoundingClientRect = () =>
({ left: 10, top: 10, width: 132, height: 84 }) as DOMRect;
secondMinimap.getBoundingClientRect = () =>
({ left: 300, top: 200, width: 132, height: 84 }) as DOMRect;
firstViewport.append(firstMinimap);
secondViewport.append(secondMinimap);
const firstRef = createRef<HTMLDivElement>();
const secondRef = createRef<HTMLDivElement>();
firstRef.current = firstViewport;
secondRef.current = secondViewport;
const first = renderHook(() =>
useImageCanvasViewportControls({
canvasViewportRef: firstRef,
layers: [layer('a', 0), layer('b', 500)],
captureCanvasHistory: vi.fn(),
}),
);
const second = renderHook(() =>
useImageCanvasViewportControls({
canvasViewportRef: secondRef,
layers: [layer('c', 1000), layer('d', 1600)],
captureCanvasHistory: vi.fn(),
}),
);
const firstBefore = first.result.current.viewport;
const secondBefore = second.result.current.viewport;
act(() => first.result.current.moveViewportFromMinimapPointer(130, 80));
expect(first.result.current.viewport).not.toEqual(firstBefore);
expect(second.result.current.viewport).toEqual(secondBefore);
});
});