a03f13d0a5
原问题
```
历史角色动作是一个兼容例外:
后端允许它没有 editor_project_resource 资源行,只要布局自身保存了完整图片序列帧。
```
- 把生成动作的原视频asstetkind改为 video, 只把图片序列作为action
- db asset新增字段image_sequence_frames_json image_seq_duration_ms等, (原来动作的这些数据存在generation-input中,并不合适)
修改了externaljob,把这部分数据正确填写到数据库中。
- 为避免到处fallback,做了数据库迁移, 脚本: `scripts/spacetime-normalize-editor-character-actions.mjs` 手动进行过画布+素材库中动作序列帧+原始视频 迁移的测试
- 移除preview_video_path字段, reason:
>preview video
→ separate resource/asset with assetKind="video"
final transparent action
→ assetKind="character-animation"
→ source_resource_id points to the preview-video resource
→ image_sequence_frames_json contains the actual playable result
>
> So preview_video_path on the final action duplicates the source video resource’s image_src.
- 移除每个frame的index字段 原因: 这个只用于后端内部处理时有一个并发请求, 每个赋一个index方便收集, 后续没有再用到,且与数组本身重复
- 清除副产品preview video的generation_input_json, 因为会影响改造功能, 迁移后预览视频不提供改造(参数), 只有序列帧动作有改造
仍存在的共性问题: #134
- 导出:
下载改为完整序列帧, 封面不再作为fallback, 部分帧读取失败时行为:仍生成 ZIP,并记录失败帧, 不变
画布放置:
before:

after:

精选模块: 主页展示, 审核部分UI:


这两处序列帧们的加载设计为惰式的, 只有hover和单独preview才会全部加载
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/117
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
765 lines
21 KiB
TypeScript
765 lines
21 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import {
|
|
act,
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
within,
|
|
} from '@testing-library/react';
|
|
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
|
|
|
import {
|
|
getAdminAssetReadUrl,
|
|
getAdminEditorShowcaseCampaign,
|
|
listAdminEditorShowcaseAssets,
|
|
reviewAdminEditorShowcaseAsset,
|
|
updateAdminEditorShowcaseDisplay,
|
|
uploadAdminEditorShowcaseCampaignImage,
|
|
upsertAdminEditorShowcaseCampaign,
|
|
} from '../api/adminApiClient';
|
|
import type { AdminEditorShowcaseAssetPayload } from '../api/adminApiTypes';
|
|
import { AdminEditorShowcaseReviewPage } from './AdminEditorShowcaseReviewPage';
|
|
|
|
vi.mock('../api/adminApiClient', () => ({
|
|
getAdminAssetReadUrl: vi.fn(),
|
|
isAdminApiError: vi.fn(
|
|
(error: unknown) =>
|
|
typeof error === 'object' &&
|
|
error !== null &&
|
|
'status' in error &&
|
|
typeof error.status === 'number',
|
|
),
|
|
getAdminEditorShowcaseCampaign: vi.fn(),
|
|
listAdminEditorShowcaseAssets: vi.fn(),
|
|
reviewAdminEditorShowcaseAsset: vi.fn(),
|
|
updateAdminEditorShowcaseDisplay: vi.fn(),
|
|
uploadAdminEditorShowcaseCampaignImage: vi.fn(),
|
|
upsertAdminEditorShowcaseCampaign: vi.fn(),
|
|
}));
|
|
|
|
interface MockIntersectionObserverController {
|
|
enter: (target: Element) => void;
|
|
isObserved: (target: Element) => boolean;
|
|
}
|
|
|
|
function installIntersectionObserverMock(): MockIntersectionObserverController {
|
|
const observed = new Map<
|
|
Element,
|
|
{
|
|
callback: IntersectionObserverCallback;
|
|
observer: IntersectionObserver;
|
|
}
|
|
>();
|
|
|
|
class MockIntersectionObserver implements IntersectionObserver {
|
|
readonly root = null;
|
|
readonly rootMargin: string;
|
|
readonly thresholds = [0];
|
|
private readonly targets = new Set<Element>();
|
|
|
|
constructor(
|
|
private readonly callback: IntersectionObserverCallback,
|
|
options: IntersectionObserverInit = {},
|
|
) {
|
|
this.rootMargin = options.rootMargin ?? '0px';
|
|
}
|
|
|
|
observe(target: Element) {
|
|
this.targets.add(target);
|
|
observed.set(target, {
|
|
callback: this.callback,
|
|
observer: this as unknown as IntersectionObserver,
|
|
});
|
|
}
|
|
|
|
unobserve(target: Element) {
|
|
this.targets.delete(target);
|
|
observed.delete(target);
|
|
}
|
|
|
|
disconnect() {
|
|
this.targets.forEach((target) => observed.delete(target));
|
|
this.targets.clear();
|
|
}
|
|
|
|
takeRecords() {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
vi.stubGlobal('IntersectionObserver', MockIntersectionObserver);
|
|
|
|
return {
|
|
enter(target) {
|
|
const record = observed.get(target);
|
|
if (!record) {
|
|
throw new Error('目标精选缩略图尚未进入 IntersectionObserver');
|
|
}
|
|
act(() => {
|
|
record.callback(
|
|
[
|
|
{
|
|
isIntersecting: true,
|
|
target,
|
|
} as IntersectionObserverEntry,
|
|
],
|
|
record.observer,
|
|
);
|
|
});
|
|
},
|
|
isObserved(target) {
|
|
return observed.has(target);
|
|
},
|
|
};
|
|
}
|
|
|
|
vi.mock('../components/AdminUserReferenceButton', () => ({
|
|
AdminUserReferenceButton: ({
|
|
userId,
|
|
publicUserCode,
|
|
}: {
|
|
userId?: string;
|
|
publicUserCode?: string | null;
|
|
}) => (
|
|
<button
|
|
aria-label="查看精选作者"
|
|
data-public-user-code={publicUserCode ?? undefined}
|
|
data-user-id={userId}
|
|
type="button"
|
|
/>
|
|
),
|
|
}));
|
|
|
|
const pendingShowcaseAsset: AdminEditorShowcaseAssetPayload = {
|
|
showcaseId: 'showcase-1',
|
|
assetId: 'asset-1',
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '作者昵称',
|
|
authorPublicUserCode: 'SY-00000042',
|
|
label: '角色形象 1',
|
|
imageSrc: '/generated-character-drafts/editor/spec.png',
|
|
objectKey: 'generated-character-drafts/editor/spec.png',
|
|
width: 1024,
|
|
height: 1024,
|
|
prompt: '完整提示词内容',
|
|
actualPrompt: null,
|
|
model: 'gpt-image-2',
|
|
provider: 'character',
|
|
taskId: 'task-1',
|
|
assetKind: 'character',
|
|
generationInputs: { style: 'clay' },
|
|
generationCostMudPoints: 12,
|
|
refundMudPoints: 6,
|
|
reviewStatus: 'pending',
|
|
displayEnabled: false,
|
|
likeCount: 0,
|
|
assetDeletedWhilePending: false,
|
|
reviewedByAdminUserId: null,
|
|
reviewNote: null,
|
|
refundLedgerId: null,
|
|
refundCompletedAt: null,
|
|
submittedAt: '2026-07-04T10:00:00Z',
|
|
reviewedAt: null,
|
|
approvedAt: null,
|
|
rejectedAt: null,
|
|
updatedAt: '2026-07-04T10:00:00Z',
|
|
showcaseCategory: null,
|
|
};
|
|
|
|
const approvedShowcaseAsset: AdminEditorShowcaseAssetPayload = {
|
|
...pendingShowcaseAsset,
|
|
showcaseId: 'showcase-2',
|
|
assetId: 'asset-2',
|
|
label: '角色形象 2',
|
|
reviewStatus: 'approved',
|
|
displayEnabled: true,
|
|
showcaseCategory: 'characters',
|
|
reviewedAt: '2026-07-04T10:10:00Z',
|
|
approvedAt: '2026-07-04T10:10:00Z',
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(listAdminEditorShowcaseAssets).mockResolvedValue({
|
|
entries: [pendingShowcaseAsset],
|
|
nextCursor: null,
|
|
});
|
|
vi.mocked(getAdminEditorShowcaseCampaign).mockResolvedValue({
|
|
campaign: {
|
|
enabled: true,
|
|
title: '活动卡',
|
|
imageSrc: '/campaign.png',
|
|
imageObjectKey: null,
|
|
imageWidth: null,
|
|
imageHeight: null,
|
|
prompt: '活动提示词',
|
|
author: '官方',
|
|
costText: '12 泥点',
|
|
updatedAt: '2026-07-04T10:00:00Z',
|
|
},
|
|
});
|
|
vi.mocked(getAdminAssetReadUrl).mockResolvedValue({
|
|
read: {
|
|
objectKey: 'generated-character-drafts/editor/spec.png',
|
|
signedUrl: 'https://signed.example.com/spec.png',
|
|
expiresAt: '2026-07-04T11:00:00Z',
|
|
},
|
|
});
|
|
vi.mocked(reviewAdminEditorShowcaseAsset).mockResolvedValue({
|
|
entry: {
|
|
...pendingShowcaseAsset,
|
|
reviewStatus: 'approved',
|
|
displayEnabled: false,
|
|
reviewedAt: '2026-07-04T10:10:00Z',
|
|
approvedAt: '2026-07-04T10:10:00Z',
|
|
},
|
|
});
|
|
vi.mocked(updateAdminEditorShowcaseDisplay).mockResolvedValue({
|
|
entry: {
|
|
...approvedShowcaseAsset,
|
|
displayEnabled: false,
|
|
},
|
|
});
|
|
vi.mocked(upsertAdminEditorShowcaseCampaign).mockResolvedValue({
|
|
campaign: {
|
|
enabled: false,
|
|
title: '新活动卡',
|
|
imageSrc: '/new-campaign.png',
|
|
imageObjectKey: null,
|
|
imageWidth: null,
|
|
imageHeight: null,
|
|
prompt: '新活动提示词',
|
|
author: '官方',
|
|
costText: '6 泥点',
|
|
updatedAt: '2026-07-04T10:20:00Z',
|
|
},
|
|
});
|
|
vi.mocked(uploadAdminEditorShowcaseCampaignImage).mockResolvedValue({
|
|
imageSrc: '/generated-character-drafts/editor/showcase-campaign/card.png',
|
|
imageObjectKey:
|
|
'generated-character-drafts/editor/showcase-campaign/card.png',
|
|
imageWidth: 900,
|
|
imageHeight: 1200,
|
|
legacyPublicPath:
|
|
'/generated-character-drafts/editor/showcase-campaign/card.png',
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
test('后台精选审核展示待审核素材和活动卡配置', async () => {
|
|
render(
|
|
<AdminEditorShowcaseReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(await screen.findByText('作者昵称')).toBeTruthy();
|
|
expect(screen.getByText('SY-00000042')).toBeTruthy();
|
|
const userButton = screen.getByRole('button', { name: '查看精选作者' });
|
|
expect(userButton.getAttribute('data-user-id')).toBe('user-1');
|
|
expect(userButton.getAttribute('data-public-user-code')).toBe('SY-00000042');
|
|
expect(screen.getAllByText('待审核').length).toBeGreaterThanOrEqual(2);
|
|
expect(screen.getByText('12 泥点')).toBeTruthy();
|
|
expect(await screen.findByDisplayValue('活动卡')).toBeTruthy();
|
|
expect(screen.getByLabelText('状态')).toHaveProperty('value', 'pending');
|
|
expect(listAdminEditorShowcaseAssets).toHaveBeenCalledWith('admin-token', {
|
|
ownerUserId: null,
|
|
reviewStatus: 'pending',
|
|
submittedAfter: null,
|
|
submittedBefore: null,
|
|
limit: 80,
|
|
});
|
|
await waitFor(() => {
|
|
expect(getAdminAssetReadUrl).toHaveBeenCalledWith('admin-token', {
|
|
objectKey: 'generated-character-drafts/editor/spec.png',
|
|
expireSeconds: 300,
|
|
});
|
|
});
|
|
});
|
|
|
|
test('后台精选审核缩略图进入视口后换签并可打开图片预览', async () => {
|
|
const observer = installIntersectionObserverMock();
|
|
render(
|
|
<AdminEditorShowcaseReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(await screen.findByText('角色形象 1')).toBeTruthy();
|
|
const previewButton = screen.getByTitle('预览素材');
|
|
const thumbnail = previewButton.querySelector('.admin-asset-query-thumb');
|
|
expect(thumbnail).not.toBeNull();
|
|
await waitFor(() => expect(observer.isObserved(thumbnail!)).toBe(true));
|
|
expect(getAdminAssetReadUrl).not.toHaveBeenCalled();
|
|
|
|
observer.enter(thumbnail!);
|
|
const image = await screen.findByRole('img', {
|
|
name: '精选素材:角色形象 1',
|
|
});
|
|
expect(image.getAttribute('src')).toBe('https://signed.example.com/spec.png');
|
|
|
|
fireEvent.click(previewButton);
|
|
const dialog = await screen.findByRole('dialog', { name: '素材预览' });
|
|
const previewImage = await within(dialog).findByRole('img', {
|
|
name: '图片预览:角色形象 1',
|
|
});
|
|
await waitFor(() => {
|
|
expect(previewImage.getAttribute('src')).toBe(
|
|
'https://signed.example.com/spec.png',
|
|
);
|
|
});
|
|
expect(screen.queryByRole('dialog', { name: '精选素材详情' })).toBeNull();
|
|
});
|
|
|
|
test('后台精选审核在共用预览弹窗播放完整角色动作', async () => {
|
|
vi.mocked(listAdminEditorShowcaseAssets).mockResolvedValueOnce({
|
|
entries: [
|
|
{
|
|
...pendingShowcaseAsset,
|
|
label: '待机动作',
|
|
assetKind: 'character-animation',
|
|
imageSrc: '/generated/action/frame-01.png',
|
|
objectKey: 'generated/action/frame-01.png',
|
|
imageSequenceFrames: [
|
|
{
|
|
imageSrc: '/generated/action/frame-01.png',
|
|
objectKey: 'generated/action/frame-01.png',
|
|
width: 192,
|
|
height: 256,
|
|
},
|
|
{
|
|
imageSrc: '/generated/action/frame-02.png',
|
|
objectKey: 'generated/action/frame-02.png',
|
|
width: 192,
|
|
height: 256,
|
|
},
|
|
],
|
|
imageSequenceDurationMs: 250,
|
|
},
|
|
],
|
|
nextCursor: null,
|
|
});
|
|
vi.mocked(getAdminAssetReadUrl).mockImplementation(
|
|
async (_token, request) => {
|
|
const objectKey = request.objectKey ?? undefined;
|
|
return {
|
|
read: {
|
|
objectKey,
|
|
signedUrl: `https://signed.example.com/${objectKey ?? ''}`,
|
|
expiresAt: '2099-01-01T00:05:00Z',
|
|
},
|
|
};
|
|
},
|
|
);
|
|
|
|
render(
|
|
<AdminEditorShowcaseReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
fireEvent.click(await screen.findByTitle('预览素材'));
|
|
|
|
const dialog = await screen.findByRole('dialog', { name: '素材预览' });
|
|
expect(
|
|
await within(dialog).findByRole('button', { name: '暂停角色动作' }),
|
|
).toBeTruthy();
|
|
await waitFor(() => {
|
|
expect(
|
|
dialog.querySelectorAll('.admin-asset-query-sequence-frame'),
|
|
).toHaveLength(2);
|
|
});
|
|
expect(getAdminAssetReadUrl).toHaveBeenCalledWith('admin-token', {
|
|
objectKey: 'generated/action/frame-02.png',
|
|
expireSeconds: 300,
|
|
});
|
|
});
|
|
|
|
test('后台精选审核对损坏角色动作显示错误而不回退首帧', async () => {
|
|
vi.mocked(listAdminEditorShowcaseAssets).mockResolvedValueOnce({
|
|
entries: [
|
|
{
|
|
...pendingShowcaseAsset,
|
|
label: '损坏动作',
|
|
assetKind: 'character-animation',
|
|
imageSequenceFrames: null,
|
|
imageSequenceDurationMs: null,
|
|
},
|
|
],
|
|
nextCursor: null,
|
|
});
|
|
|
|
render(
|
|
<AdminEditorShowcaseReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
fireEvent.click(await screen.findByTitle('预览素材'));
|
|
|
|
const dialog = await screen.findByRole('dialog', { name: '素材预览' });
|
|
expect(
|
|
within(dialog).getByLabelText('角色动作序列损坏:损坏动作'),
|
|
).toBeTruthy();
|
|
expect(
|
|
within(dialog).queryByRole('img', { name: '图片预览:损坏动作' }),
|
|
).toBeNull();
|
|
});
|
|
|
|
test('后台精选审核将无 objectKey 的绝对 OSS 图片地址换签后预览', async () => {
|
|
vi.mocked(listAdminEditorShowcaseAssets).mockResolvedValueOnce({
|
|
entries: [
|
|
{
|
|
...pendingShowcaseAsset,
|
|
imageSrc:
|
|
'https://genarrative.oss-cn-shanghai.aliyuncs.com/generated-character-drafts/editor/absolute.png?x-oss-process=image/resize,w_320',
|
|
objectKey: null,
|
|
},
|
|
],
|
|
nextCursor: null,
|
|
});
|
|
vi.mocked(getAdminAssetReadUrl).mockResolvedValue({
|
|
read: {
|
|
objectKey: 'generated-character-drafts/editor/absolute.png',
|
|
signedUrl: 'https://signed.example.com/absolute.png',
|
|
expiresAt: '2026-07-04T11:00:00Z',
|
|
},
|
|
});
|
|
|
|
render(
|
|
<AdminEditorShowcaseReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const image = await screen.findByRole('img', {
|
|
name: '精选素材:角色形象 1',
|
|
});
|
|
expect(image.getAttribute('src')).toBe(
|
|
'https://signed.example.com/absolute.png',
|
|
);
|
|
expect(getAdminAssetReadUrl).toHaveBeenCalledWith('admin-token', {
|
|
legacyPublicPath: '/generated-character-drafts/editor/absolute.png',
|
|
expireSeconds: 300,
|
|
});
|
|
|
|
fireEvent.click(screen.getByTitle('预览素材'));
|
|
const dialog = await screen.findByRole('dialog', { name: '素材预览' });
|
|
expect(
|
|
await within(dialog).findByRole('img', {
|
|
name: '图片预览:角色形象 1',
|
|
}),
|
|
).toHaveProperty('src', 'https://signed.example.com/absolute.png');
|
|
});
|
|
|
|
test('后台精选审核详情中的缩略图也可打开素材预览', async () => {
|
|
render(
|
|
<AdminEditorShowcaseReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(await screen.findByRole('button', { name: '详情' }));
|
|
const detail = await screen.findByRole('dialog', { name: '精选素材详情' });
|
|
fireEvent.click(within(detail).getByTitle('预览素材'));
|
|
|
|
const preview = await screen.findByRole('dialog', { name: '素材预览' });
|
|
expect(
|
|
await within(preview).findByRole('img', {
|
|
name: '图片预览:角色形象 1',
|
|
}),
|
|
).toBeTruthy();
|
|
});
|
|
|
|
test('后台精选审核格式化微秒时间并显示素材名', async () => {
|
|
vi.mocked(listAdminEditorShowcaseAssets).mockResolvedValueOnce({
|
|
entries: [
|
|
{
|
|
...pendingShowcaseAsset,
|
|
submittedAt: '1783231493.573727Z',
|
|
},
|
|
],
|
|
nextCursor: null,
|
|
});
|
|
|
|
render(
|
|
<AdminEditorShowcaseReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(await screen.findByText('角色形象 1')).toBeTruthy();
|
|
expect(screen.queryByText('showcase-1')).toBeNull();
|
|
expect(screen.queryByText('1783231493.573727Z')).toBeNull();
|
|
});
|
|
|
|
test('后台精选审核音频素材使用统一封面缩略图', async () => {
|
|
vi.mocked(listAdminEditorShowcaseAssets).mockResolvedValueOnce({
|
|
entries: [
|
|
{
|
|
...pendingShowcaseAsset,
|
|
showcaseId: 'showcase-audio-1',
|
|
assetId: 'asset-audio-1',
|
|
label: '胜利音效',
|
|
imageSrc: '/generated-editor-audios/sfx.mp3',
|
|
objectKey: 'generated-editor-audios/sfx.mp3',
|
|
assetKind: 'sound-effect',
|
|
},
|
|
],
|
|
nextCursor: null,
|
|
});
|
|
|
|
render(
|
|
<AdminEditorShowcaseReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const image = await screen.findByRole('img', { name: '精选素材:胜利音效' });
|
|
expect(image.getAttribute('src')).toBe(
|
|
'/creation-home/audio-asset-cover.png',
|
|
);
|
|
expect(getAdminAssetReadUrl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('后台精选审核可以通过素材并查看完整提示词', async () => {
|
|
render(
|
|
<AdminEditorShowcaseReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.change(await screen.findByLabelText('审核备注:角色形象 1'), {
|
|
target: { value: '质量通过' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: '通过' }));
|
|
|
|
await waitFor(() => {
|
|
expect(reviewAdminEditorShowcaseAsset).toHaveBeenCalledWith('admin-token', {
|
|
showcaseId: 'showcase-1',
|
|
reviewStatus: 'approved',
|
|
reviewNote: '质量通过',
|
|
});
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '完整提示词内容' }));
|
|
const dialog = screen.getByRole('dialog', { name: '完整提示词' });
|
|
expect(within(dialog).getByText('完整提示词内容')).toBeTruthy();
|
|
});
|
|
|
|
test('后台精选审核可以切换展示和保存活动卡', async () => {
|
|
vi.mocked(listAdminEditorShowcaseAssets).mockResolvedValueOnce({
|
|
entries: [approvedShowcaseAsset],
|
|
nextCursor: null,
|
|
});
|
|
|
|
render(
|
|
<AdminEditorShowcaseReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(await screen.findByRole('button', { name: '隐藏' }));
|
|
await waitFor(() => {
|
|
expect(updateAdminEditorShowcaseDisplay).toHaveBeenCalledWith(
|
|
'admin-token',
|
|
{
|
|
showcaseId: 'showcase-2',
|
|
displayEnabled: false,
|
|
showcaseCategory: 'characters',
|
|
},
|
|
);
|
|
});
|
|
|
|
fireEvent.change(await screen.findByLabelText('标题'), {
|
|
target: { value: '新活动卡' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('图片地址'), {
|
|
target: { value: '/new-campaign.png' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('成本文案'), {
|
|
target: { value: '6 泥点' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: '保存活动卡' }));
|
|
|
|
await waitFor(() => {
|
|
expect(upsertAdminEditorShowcaseCampaign).toHaveBeenCalledWith(
|
|
'admin-token',
|
|
expect.objectContaining({
|
|
title: '新活动卡',
|
|
imageSrc: '/new-campaign.png',
|
|
imageObjectKey: null,
|
|
costText: '6 泥点',
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
test('后台精选活动卡上传图片时替换图片引用并隐藏 objectKey', async () => {
|
|
render(
|
|
<AdminEditorShowcaseReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
await screen.findByDisplayValue('活动卡');
|
|
const file = new File(['image-bytes'], 'card.png', { type: 'image/png' });
|
|
fireEvent.change(screen.getByLabelText('上传活动卡图片'), {
|
|
target: { files: [file] },
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(uploadAdminEditorShowcaseCampaignImage).toHaveBeenCalledWith(
|
|
'admin-token',
|
|
file,
|
|
);
|
|
});
|
|
expect(
|
|
await screen.findByDisplayValue(
|
|
'/generated-character-drafts/editor/showcase-campaign/card.png',
|
|
),
|
|
).toBeTruthy();
|
|
expect(
|
|
screen.queryByDisplayValue(
|
|
'generated-character-drafts/editor/showcase-campaign/card.png',
|
|
),
|
|
).toBeNull();
|
|
|
|
vi.mocked(uploadAdminEditorShowcaseCampaignImage).mockResolvedValueOnce({
|
|
imageSrc:
|
|
'/generated-character-drafts/editor/showcase-campaign/replacement.png',
|
|
imageObjectKey:
|
|
'generated-character-drafts/editor/showcase-campaign/replacement.png',
|
|
imageWidth: 1024,
|
|
imageHeight: 1536,
|
|
legacyPublicPath:
|
|
'/generated-character-drafts/editor/showcase-campaign/replacement.png',
|
|
});
|
|
const replacementFile = new File(['replacement-bytes'], 'replacement.png', {
|
|
type: 'image/png',
|
|
});
|
|
fireEvent.change(screen.getByLabelText('上传活动卡图片'), {
|
|
target: { files: [replacementFile] },
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(uploadAdminEditorShowcaseCampaignImage).toHaveBeenCalledWith(
|
|
'admin-token',
|
|
replacementFile,
|
|
);
|
|
});
|
|
expect(
|
|
await screen.findByDisplayValue(
|
|
'/generated-character-drafts/editor/showcase-campaign/replacement.png',
|
|
),
|
|
).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '保存活动卡' }));
|
|
|
|
await waitFor(() => {
|
|
expect(upsertAdminEditorShowcaseCampaign).toHaveBeenCalledWith(
|
|
'admin-token',
|
|
expect.objectContaining({
|
|
imageSrc:
|
|
'/generated-character-drafts/editor/showcase-campaign/replacement.png',
|
|
imageObjectKey:
|
|
'generated-character-drafts/editor/showcase-campaign/replacement.png',
|
|
imageWidth: 1024,
|
|
imageHeight: 1536,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
test('后台精选审核已通过素材可以设置精选分类', async () => {
|
|
vi.mocked(listAdminEditorShowcaseAssets).mockResolvedValueOnce({
|
|
entries: [
|
|
{
|
|
...approvedShowcaseAsset,
|
|
showcaseCategory: null,
|
|
displayEnabled: false,
|
|
},
|
|
],
|
|
nextCursor: null,
|
|
});
|
|
vi.mocked(updateAdminEditorShowcaseDisplay).mockResolvedValueOnce({
|
|
entry: {
|
|
...approvedShowcaseAsset,
|
|
showcaseCategory: 'ui',
|
|
displayEnabled: false,
|
|
},
|
|
});
|
|
|
|
render(
|
|
<AdminEditorShowcaseReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.change(await screen.findByLabelText('精选分类:角色形象 2'), {
|
|
target: { value: 'ui' },
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(updateAdminEditorShowcaseDisplay).toHaveBeenCalledWith(
|
|
'admin-token',
|
|
{
|
|
showcaseId: 'showcase-2',
|
|
displayEnabled: false,
|
|
showcaseCategory: 'ui',
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
test('后台精选审核清空精选分类时不会自动隐藏素材', async () => {
|
|
vi.mocked(listAdminEditorShowcaseAssets).mockResolvedValueOnce({
|
|
entries: [approvedShowcaseAsset],
|
|
nextCursor: null,
|
|
});
|
|
vi.mocked(updateAdminEditorShowcaseDisplay).mockResolvedValueOnce({
|
|
entry: {
|
|
...approvedShowcaseAsset,
|
|
showcaseCategory: null,
|
|
displayEnabled: true,
|
|
},
|
|
});
|
|
|
|
render(
|
|
<AdminEditorShowcaseReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.change(await screen.findByLabelText('精选分类:角色形象 2'), {
|
|
target: { value: '' },
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(updateAdminEditorShowcaseDisplay).toHaveBeenCalledWith(
|
|
'admin-token',
|
|
{
|
|
showcaseId: 'showcase-2',
|
|
displayEnabled: true,
|
|
showcaseCategory: '',
|
|
},
|
|
);
|
|
});
|
|
});
|