Files
Genarrative/packages/image-canvas-react/src/sharedCanvasReact.test.tsx
T
k88936 7288c6f641
Project CI / Repository checks (push) Successful in 2m59s
Project CI / Frontend tests (push) Successful in 3m29s
Project CI / Backend tests (push) Successful in 8m37s
Project CI / Native shell tests (push) Successful in 20m55s
Fix/修复无限画布图标文字模糊 (#285)
closes #279
closes #284

实现: 从2(暂定)倍放大绘制的画布scale 0.5* 真正的scale

before:
![shotmd-1788579338.jpg](/attachments/9d44ec5e-454c-449b-86bc-b2131fbbc8cc)
![shotmd-1788578910.jpg](/attachments/c9da61a2-b467-43eb-a5de-e5e4d5cb8f70)

after:
![shotmd-1788580930.jpg](/attachments/f1f06e8e-90bf-45e2-a30b-a63ccc308c19)
![shotmd-1788583400.jpg](/attachments/c48ca741-e470-42b3-8030-064e0579f63a)
svg看起来stroke窄了一点点, 可以接受

Reviewed-on: #285
2026-09-09 16:32:43 +08:00

172 lines
5.3 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 {
CANVAS_RENDER_SUPERSAMPLE,
canvasViewportToWorldTransform,
} from './canvasWorldRendering';
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(
canvasViewportToWorldTransform({ x: 12, y: 24, scale: 2 }),
);
expect(
world?.style.getPropertyValue('--genarrative-image-canvas-inverse-scale'),
).toBe('0.5');
expect(content?.style.zoom).toBe(String(CANVAS_RENDER_SUPERSAMPLE));
});
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);
});
});