Files
Genarrative/src/components/image-editor/ImageCanvasTopbarView.test.tsx
T
k88936 6ee33dc561
Project CI / Repository checks (push) Successful in 1m4s
Project CI / Frontend tests (push) Successful in 2m9s
Project CI / Native shell tests (push) Successful in 2m38s
Project CI / Backend tests (push) Successful in 3m23s
fix: 首页「最近项目」缩略图与退出前视角不一致,出现偏移或裁切 (#111)
- 封面使用真实画布尺寸及最终 viewport.x/y/scale。
- 320×240 封面改为居中 cover 裁切,不再从左上角取景。
- 正确绘制画布背景色。

in canva:
![shotmd-1784874081.jpg](/attachments/4ac128cc-64c8-4a78-9e75-c35902049d3e)
![shotmd-1784876202.jpg](/attachments/6197259b-887e-4fd6-95b5-b722ec670a02)

in home:
![shotmd-1784874090.jpg](/attachments/241c7cd3-1eee-4f7b-96f2-4ca0268d2bee)
![shotmd-1784876211.jpg](/attachments/aa4200b6-bef0-4122-a797-ebc082b27a17)

因为原本的布局持久化可能有一些bug,改动不相关,所以我回退到最开始的一版:
- 不额外监听画布尺寸变化,给最小尺寸兜底,(大了比小了好)
- 封面不要求强一致性,生成/上传时机仍然按照原来随布局持久化

优化了封面生成:
- 改用webp格式
- 尽量利用图片缓存

最终生成封面从700ms加速到150ms以内, 加上必须的上传和确认150ms 和 布局持久化的时间相差不多,不是特别离谱的手速都能保证两者一致

---------

Co-authored-by: kdletters <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/111
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-07-29 16:09:30 +08:00

278 lines
8.6 KiB
TypeScript

/* @vitest-environment jsdom */
import {
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
import { ImageCanvasTopbarView } from './ImageCanvasTopbarView';
import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts';
const walletBreakdown = {
totalPoints: 1_234,
permanentPoints: 1_000,
limitedPoints: 214,
limitedExpiresAt: '2026-07-31T16:00:00Z',
dailyFreePoints: 20,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-07-12T16:00:00Z',
};
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
const id = overrides.id ?? 'layer-a';
return {
id,
resourceId: `resource-${id}`,
title: id,
src: `data:image/png;base64,${id}`,
x: 0,
y: 0,
width: 320,
height: 180,
originalWidth: 320,
originalHeight: 180,
zIndex: 1,
sourceType: 'uploaded',
...overrides,
};
}
function renderTopbar(
overrides: Partial<Parameters<typeof ImageCanvasTopbarView>[0]> = {},
) {
useImageCanvasContextStore.getState().setProjectId('project-a');
const props: Parameters<typeof ImageCanvasTopbarView>[0] = {
projectTitle: '默认项目',
projectRenameValue: '默认项目',
isRenamingProject: false,
isProjectRenameSaving: false,
projectRenameError: null,
layers: [],
walletBalance: 1_234,
walletBreakdown,
isWalletBalanceLoading: false,
isWalletDetailsLoading: false,
walletDetailsError: null,
currentUser: null,
assetExportStatus: null,
isExportingAssets: false,
setProjectRenameValue: vi.fn(),
startProjectRename: vi.fn(),
cancelProjectRename: vi.fn(),
submitProjectRename: vi.fn(),
resetProjectRenameError: vi.fn(),
exportCanvasAssets: vi.fn(),
onOpenShortcuts: vi.fn(),
onRequestWalletDetails: vi.fn(),
onRecharge: vi.fn(),
onOpenWalletLedger: vi.fn(),
onOpenAccount: vi.fn(),
onReturnToProjects: vi.fn(),
...overrides,
};
render(<ImageCanvasTopbarView {...props} />);
return props;
}
describe('ImageCanvasTopbarView', () => {
it('shows the project title and project gallery link', () => {
const props = renderTopbar();
expect(screen.getByRole('heading', { name: '默认项目' })).toBeTruthy();
expect(screen.queryByText('画布')).toBeNull();
expect(
screen.getByRole('link', { name: '返回项目页面' }).getAttribute('href'),
).toBe('/project');
fireEvent.click(screen.getByRole('button', { name: '编辑项目名称' }));
expect(props.startProjectRename).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByRole('button', { name: '查看画布快捷键' }));
expect(props.onOpenShortcuts).toHaveBeenCalledTimes(1);
});
it('shows the shared mud point breakdown and wallet actions', async () => {
const user = userEvent.setup();
const props = renderTopbar({
walletBalance: 12_345,
walletBreakdown: {
...walletBreakdown,
totalPoints: 12_345,
},
});
const walletChip = screen.getByRole('button', {
name: '泥点 12,345',
});
expect(walletChip.textContent).toBe('泥点 1.2万');
await user.hover(walletChip);
const details = screen.getByRole('dialog', { name: '泥点账户详情' });
expect(within(details).getByText('不限时泥点')).toBeTruthy();
expect(within(details).queryByText('限时泥点')).toBeNull();
expect(within(details).getByText('每日免费泥点')).toBeTruthy();
await user.click(within(details).getByRole('button', { name: '使用详情' }));
expect(props.onOpenWalletLedger).toHaveBeenCalledTimes(1);
await user.click(screen.getByRole('button', { name: '充值' }));
expect(props.onRecharge).toHaveBeenCalledTimes(1);
expect(props.onRequestWalletDetails).toHaveBeenCalledTimes(1);
});
it('waits for project persistence before allowing another return request', async () => {
let resolveReturn!: () => void;
const pendingReturn = new Promise<void>((resolve) => {
resolveReturn = resolve;
});
const onReturnToProjects = vi.fn(() => pendingReturn);
renderTopbar({ onReturnToProjects });
const projectLink = screen.getByRole('link', {
name: '返回项目页面',
});
fireEvent.click(projectLink);
fireEvent.click(projectLink);
expect(onReturnToProjects).toHaveBeenCalledTimes(1);
expect(projectLink.getAttribute('aria-disabled')).toBe('true');
resolveReturn();
await waitFor(() => {
expect(projectLink.getAttribute('aria-disabled')).toBe('false');
});
});
it('shows the current user avatar beside the mud point balance', () => {
const props = renderTopbar({
currentUser: {
id: 'user-1',
publicUserCode: 'SY-00000003',
displayName: '厉冰都',
avatarUrl: 'data:image/png;base64,AVATAR',
},
});
const accountButton = screen.getByRole('button', { name: '账号 厉冰都' });
expect(accountButton.textContent).toContain('厉冰都');
expect(accountButton.textContent).toContain('SY-00000003');
expect(accountButton.querySelector('img')?.getAttribute('src')).toBe(
'data:image/png;base64,AVATAR',
);
fireEvent.click(accountButton);
expect(props.onOpenAccount).toHaveBeenCalledTimes(1);
});
it('submits, resets, and cancels project rename edits', () => {
const props = renderTopbar({
isRenamingProject: true,
projectRenameValue: '草稿项目',
projectRenameError: '项目名称不能为空',
});
const input = screen.getByLabelText('项目名称');
fireEvent.change(input, { target: { value: '新项目' } });
fireEvent.click(screen.getByRole('button', { name: '保存项目名称' }));
expect(props.setProjectRenameValue).toHaveBeenCalledWith('新项目');
expect(props.resetProjectRenameError).toHaveBeenCalledTimes(1);
expect(props.submitProjectRename).toHaveBeenCalledWith('project-a');
expect(screen.getByRole('alert').textContent).toBe('项目名称不能为空');
fireEvent.keyDown(input, { key: 'Escape' });
expect(props.cancelProjectRename).toHaveBeenCalledTimes(1);
});
it('exports only when canvas has exportable layers and shows export status', () => {
const exportCanvasAssets = vi.fn();
const { rerender } = render(
<ImageCanvasTopbarView
projectTitle="默认项目"
projectRenameValue="默认项目"
isRenamingProject={false}
isProjectRenameSaving={false}
projectRenameError={null}
layers={[]}
walletBalance={0}
walletBreakdown={null}
isWalletBalanceLoading={false}
isWalletDetailsLoading={false}
walletDetailsError={null}
currentUser={null}
assetExportStatus={null}
isExportingAssets={false}
setProjectRenameValue={vi.fn()}
startProjectRename={vi.fn()}
cancelProjectRename={vi.fn()}
submitProjectRename={vi.fn()}
resetProjectRenameError={vi.fn()}
exportCanvasAssets={exportCanvasAssets}
onOpenShortcuts={vi.fn()}
onRequestWalletDetails={vi.fn()}
onRecharge={vi.fn()}
onOpenWalletLedger={vi.fn()}
onOpenAccount={vi.fn()}
/>,
);
expect(
(
screen.getByRole('button', {
name: '下载画布素材',
}) as HTMLButtonElement
).disabled,
).toBe(true);
rerender(
<ImageCanvasTopbarView
projectTitle="默认项目"
projectRenameValue="默认项目"
isRenamingProject={false}
isProjectRenameSaving={false}
projectRenameError={null}
layers={[createLayer()]}
walletBalance={0}
walletBreakdown={null}
isWalletBalanceLoading={false}
isWalletDetailsLoading={false}
walletDetailsError={null}
currentUser={null}
assetExportStatus={{
tone: 'success',
message: '画布素材已导出',
}}
isExportingAssets={false}
setProjectRenameValue={vi.fn()}
startProjectRename={vi.fn()}
cancelProjectRename={vi.fn()}
submitProjectRename={vi.fn()}
resetProjectRenameError={vi.fn()}
exportCanvasAssets={exportCanvasAssets}
onOpenShortcuts={vi.fn()}
onRequestWalletDetails={vi.fn()}
onRecharge={vi.fn()}
onOpenWalletLedger={vi.fn()}
onOpenAccount={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '下载画布素材' }));
expect(exportCanvasAssets).toHaveBeenCalledTimes(1);
expect(screen.getByRole('status').textContent).toBe('画布素材已导出');
});
});