Files
Genarrative/src/components/image-editor/useImageCanvasEditorChrome.test.tsx
kdletters e67e921c67 拆分图片画布编辑器外壳状态
新增编辑器外壳状态 hook

抽出项目重命名、背景设置、侧栏和工具状态

补充外壳状态单测并更新拆分记录
2026-06-17 08:58:43 +08:00

225 lines
8.2 KiB
TypeScript

/* @vitest-environment jsdom */
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ApiClientError } from '../../services/apiClient';
import { useImageCanvasEditorChrome } from './useImageCanvasEditorChrome';
const renameEditorProjectMock = vi.hoisted(() => vi.fn());
vi.mock('../../services/image-editor/editorProjectClient', async () => {
const actual = await vi.importActual<
typeof import('../../services/image-editor/editorProjectClient')
>('../../services/image-editor/editorProjectClient');
return {
...actual,
renameEditorProject: renameEditorProjectMock,
};
});
function ChromeHarness({
openEditorLoginModal = vi.fn(),
}: {
openEditorLoginModal?: (postLoginAction?: (() => void) | null) => void;
}) {
const chrome = useImageCanvasEditorChrome({ openEditorLoginModal });
return (
<div>
<span data-testid="title">{chrome.projectTitle}</span>
<span data-testid="rename-value">{chrome.projectRenameValue}</span>
<span data-testid="renaming">{String(chrome.isRenamingProject)}</span>
<span data-testid="saving">{String(chrome.isProjectRenameSaving)}</span>
<span data-testid="rename-error">{chrome.projectRenameError ?? '-'}</span>
<span data-testid="sidebar">{chrome.activeSidebarPanel ?? '-'}</span>
<span data-testid="tool">{chrome.activeTool}</span>
<span data-testid="zoom">{String(chrome.isZoomMenuOpen)}</span>
<span data-testid="background-open">
{String(chrome.isBackgroundSettingsOpen)}
</span>
<span data-testid="minimap">{String(chrome.isMinimapOpen)}</span>
<span data-testid="background-color">
{chrome.canvasBackgroundColor}
</span>
<span data-testid="background-hex">
{chrome.canvasBackgroundHexValue}
</span>
<button type="button" onClick={chrome.startProjectRename}>
start rename
</button>
<button type="button" onClick={chrome.cancelProjectRename}>
cancel rename
</button>
<button
type="button"
onClick={() => chrome.submitProjectRename('project-1')}
>
submit rename
</button>
<button
type="button"
onClick={() => chrome.submitProjectRename(null)}
>
submit without project
</button>
<button
type="button"
onClick={() => chrome.setProjectRenameValue(' 新项目 ')}
>
set rename
</button>
<button
type="button"
onClick={() => chrome.setProjectRenameValue(' ')}
>
blank rename
</button>
<button type="button" onClick={() => chrome.setProjectTitle('已有项目')}>
set title
</button>
<button type="button" onClick={() => chrome.setActiveTool('hand')}>
set hand
</button>
<button type="button" onClick={() => chrome.toggleSidebarPanel('assets')}>
toggle assets
</button>
<button type="button" onClick={() => chrome.toggleSidebarPanel('layers')}>
toggle layers
</button>
<button type="button" onClick={chrome.toggleZoomMenu}>
toggle zoom
</button>
<button type="button" onClick={chrome.toggleBackgroundSettings}>
toggle background
</button>
<button type="button" onClick={chrome.toggleMinimap}>
toggle minimap
</button>
<button type="button" onClick={chrome.closeEditorChromePanels}>
close panels
</button>
<button
type="button"
onClick={() => chrome.applyCanvasBackgroundColor('#abc')}
>
apply short hex
</button>
<button
type="button"
onClick={() => chrome.handleCanvasBackgroundHexChange('#not-a-color')}
>
invalid hex
</button>
<button
type="button"
onClick={() => chrome.handleCanvasBackgroundHexChange('#ffffff')}
>
valid hex
</button>
</div>
);
}
describe('useImageCanvasEditorChrome', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renames projects and resets rename state from the saved title', async () => {
renameEditorProjectMock.mockResolvedValueOnce({
projectId: 'project-1',
title: '后端项目名',
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
resources: [],
updatedAt: '2026-06-17T00:00:00.000Z',
});
render(<ChromeHarness />);
fireEvent.click(screen.getByRole('button', { name: 'set title' }));
fireEvent.click(screen.getByRole('button', { name: 'start rename' }));
expect(screen.getByTestId('rename-value').textContent).toBe('已有项目');
fireEvent.click(screen.getByRole('button', { name: 'set rename' }));
fireEvent.click(screen.getByRole('button', { name: 'submit rename' }));
expect(screen.getByTestId('saving').textContent).toBe('true');
await waitFor(() => {
expect(renameEditorProjectMock).toHaveBeenCalledWith(
'project-1',
'新项目',
);
});
expect(screen.getByTestId('title').textContent).toBe('后端项目名');
expect(screen.getByTestId('rename-value').textContent).toBe('后端项目名');
expect(screen.getByTestId('renaming').textContent).toBe('false');
expect(screen.getByTestId('saving').textContent).toBe('false');
});
it('validates rename input and opens login on rename auth errors', async () => {
const openEditorLoginModal = vi.fn();
renameEditorProjectMock.mockRejectedValueOnce(
new ApiClientError({
message: '未授权访问',
status: 401,
code: 'UNAUTHORIZED',
}),
);
render(<ChromeHarness openEditorLoginModal={openEditorLoginModal} />);
fireEvent.click(screen.getByRole('button', { name: 'blank rename' }));
fireEvent.click(screen.getByRole('button', { name: 'submit rename' }));
expect(screen.getByTestId('rename-error').textContent).toBe(
'项目名称不能为空',
);
expect(renameEditorProjectMock).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: 'set rename' }));
fireEvent.click(screen.getByRole('button', { name: 'submit rename' }));
await waitFor(() => {
expect(openEditorLoginModal).toHaveBeenCalledTimes(1);
});
expect(screen.getByTestId('rename-error').textContent).toBe('未授权访问');
});
it('manages background colors and chrome panel toggles', () => {
render(<ChromeHarness />);
fireEvent.click(screen.getByRole('button', { name: 'apply short hex' }));
expect(screen.getByTestId('background-color').textContent).toBe('#aabbcc');
expect(screen.getByTestId('background-hex').textContent).toBe('#aabbcc');
fireEvent.click(screen.getByRole('button', { name: 'invalid hex' }));
expect(screen.getByTestId('background-color').textContent).toBe('#aabbcc');
expect(screen.getByTestId('background-hex').textContent).toBe(
'#not-a-color',
);
fireEvent.click(screen.getByRole('button', { name: 'valid hex' }));
expect(screen.getByTestId('background-color').textContent).toBe('#ffffff');
expect(screen.getByTestId('background-hex').textContent).toBe('#ffffff');
fireEvent.click(screen.getByRole('button', { name: 'toggle assets' }));
expect(screen.getByTestId('sidebar').textContent).toBe('-');
fireEvent.click(screen.getByRole('button', { name: 'toggle layers' }));
expect(screen.getByTestId('sidebar').textContent).toBe('layers');
fireEvent.click(screen.getByRole('button', { name: 'toggle zoom' }));
fireEvent.click(screen.getByRole('button', { name: 'toggle background' }));
fireEvent.click(screen.getByRole('button', { name: 'toggle minimap' }));
fireEvent.click(screen.getByRole('button', { name: 'set hand' }));
expect(screen.getByTestId('zoom').textContent).toBe('true');
expect(screen.getByTestId('background-open').textContent).toBe('true');
expect(screen.getByTestId('minimap').textContent).toBe('false');
expect(screen.getByTestId('tool').textContent).toBe('hand');
act(() => {
screen.getByRole('button', { name: 'close panels' }).click();
});
expect(screen.getByTestId('sidebar').textContent).toBe('-');
expect(screen.getByTestId('zoom').textContent).toBe('false');
expect(screen.getByTestId('background-open').textContent).toBe('false');
});
});