6ee33dc561
- 封面使用真实画布尺寸及最终 viewport.x/y/scale。 - 320×240 封面改为居中 cover 裁切,不再从左上角取景。 - 正确绘制画布背景色。 in canva:   in home:   因为原本的布局持久化可能有一些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>
278 lines
8.6 KiB
TypeScript
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('画布素材已导出');
|
|
});
|
|
});
|