Files
Genarrative/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx
T
kdletters 3f3a6c4ab2 修复画布生图交付与素材成本展示
修复 nanobanana 标量尺寸被误判为宽高的问题。

普通生图和快速编辑在内存完成尺寸变换后仅上传一次,变换失败保留 provider 原图。

移除快速编辑重复的原始输出素材,保留角色、图集和 UI 的独立中间产物。

后台将不重复计费的中间产物显示为不单独计费。

补充单次上传、降级交付和成本展示回归测试及相关文档。
2026-07-15 01:36:16 +08:00

941 lines
28 KiB
TypeScript

/* @vitest-environment jsdom */
import {
act,
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import {
getAdminAssetReadUrl,
listAdminEditorAssets,
} from '../api/adminApiClient';
import type { AdminEditorAssetPayload } from '../api/adminApiTypes';
import { AdminEditorAssetQueryPage } from './AdminEditorAssetQueryPage';
vi.mock('../api/adminApiClient', () => ({
getAdminAssetReadUrl: vi.fn(),
isAdminApiError: vi.fn(
(error: unknown) =>
typeof error === 'object' &&
error !== null &&
'status' in error &&
typeof error.status === 'number',
),
listAdminEditorAssets: 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 generatedAsset: AdminEditorAssetPayload = {
assetId: 'asset-1',
ownerUserId: 'user-1',
authorDisplayName: '作者昵称',
authorPublicUserCode: 'SY-00000042',
folderId: 'folder-1',
label: '角色形象 1',
assetObjectId: 'asset-object-1',
imageSrc: '/generated-character-drafts/editor/spec.png',
objectKey: 'generated-character-drafts/editor/spec.png',
width: 1024,
height: 1024,
sourceType: 'generated',
prompt: '完整提示词内容',
actualPrompt: null,
model: 'gpt-image-2',
provider: 'character',
taskId: 'task-1',
assetKind: 'character',
generationInputs: { style: 'clay' },
sourceResourceId: 'resource-1',
thumbnailSrc: null,
generationCostMudPoints: 12,
createdAt: '2026-07-04T10:00:00Z',
updatedAt: '2026-07-04T10:00:00Z',
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(listAdminEditorAssets).mockResolvedValue({
entries: [generatedAsset],
nextCursor: null,
});
vi.mocked(getAdminAssetReadUrl).mockResolvedValue({
read: {
objectKey: 'generated-character-drafts/editor/spec.png',
signedUrl:
'https://signed.example.com/generated-character-drafts/editor/spec.png',
expiresAt: '2026-07-04T11:00:00Z',
},
});
});
afterEach(() => {
vi.unstubAllGlobals();
vi.useRealTimers();
});
function generatedAssetAt(index: number): AdminEditorAssetPayload {
const objectKey = `generated-character-drafts/editor/spec-${index}.png`;
return {
...generatedAsset,
assetId: `asset-${index}`,
label: `角色形象 ${index}`,
imageSrc: `/${objectKey}`,
objectKey,
};
}
function thumbnailElementForLabel(label: string) {
const row = screen.getByText(label).closest('tr');
const thumbnail = row?.querySelector('.admin-asset-query-thumb');
if (!thumbnail) {
throw new Error(`未找到素材缩略图:${label}`);
}
return thumbnail;
}
test('后台素材查询展示作者昵称和陶泥号', async () => {
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
expect(await screen.findByText('作者昵称')).toBeTruthy();
expect(screen.getByText('SY-00000042')).toBeTruthy();
expect(screen.queryByText('user-1')).toBeNull();
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');
});
test('后台素材查询按用户、搜索和时间调用查询接口', async () => {
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await screen.findByRole('img', { name: '素材:角色形象 1' });
fireEvent.change(screen.getByLabelText('用户 ID'), {
target: { value: 'user-1' },
});
fireEvent.change(screen.getByLabelText('搜索'), {
target: { value: '陶泥角色' },
});
fireEvent.change(screen.getByLabelText('开始时间'), {
target: { value: '2026-07-01' },
});
fireEvent.change(screen.getByLabelText('结束时间'), {
target: { value: '2026-07-04' },
});
await waitFor(() => {
expect(listAdminEditorAssets).toHaveBeenLastCalledWith('admin-token', {
ownerUserId: 'user-1',
keyword: '陶泥角色',
createdAfter: '2026-07-01T00:00:00+08:00',
createdBefore: '2026-07-04T23:59:59.999+08:00',
limit: 80,
});
});
});
test('后台素材查询不展示分类筛选和分类列', async () => {
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
expect(await screen.findByText('角色形象 1')).toBeTruthy();
expect(screen.queryByLabelText('分类')).toBeNull();
expect(screen.queryByRole('columnheader', { name: '分类' })).toBeNull();
});
test('后台素材查询将中间产物标记为不单独计费', async () => {
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries: [
{
...generatedAsset,
assetId: 'asset-source',
label: '角色形象原图',
assetKind: 'editor_green_screen_source',
generationCostMudPoints: 0,
},
generatedAsset,
],
nextCursor: null,
});
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
const sourceRow = (await screen.findByText('角色形象原图')).closest('tr');
const finalRow = screen.getByText('角色形象 1').closest('tr');
expect(sourceRow).not.toBeNull();
expect(finalRow).not.toBeNull();
expect(within(sourceRow!).getByText('不单独计费')).toBeTruthy();
expect(within(finalRow!).getByText('12 泥点')).toBeTruthy();
fireEvent.click(within(sourceRow!).getByRole('button', { name: '详情' }));
expect(
within(await screen.findByRole('dialog', { name: '素材详情' })).getByText(
'不单独计费',
),
).toBeTruthy();
});
test('后台素材查询缩略图使用 objectKey 换签后展示', async () => {
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
const image = await screen.findByRole('img', { name: '素材:角色形象 1' });
await waitFor(() => {
expect(image.getAttribute('src')).toBe(
'https://signed.example.com/generated-character-drafts/editor/spec.png',
);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledWith('admin-token', {
objectKey: 'generated-character-drafts/editor/spec.png',
expireSeconds: 300,
});
});
test('后台素材查询只为进入可视区域的缩略图换签并能展示后续行', async () => {
const observer = installIntersectionObserverMock();
const entries = [
generatedAssetAt(1),
generatedAssetAt(2),
generatedAssetAt(3),
];
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries,
nextCursor: null,
});
vi.mocked(getAdminAssetReadUrl).mockImplementation(
async (_token, request) => ({
read: {
objectKey: request.objectKey ?? '',
signedUrl: `https://signed.example.com/${request.objectKey}`,
expiresAt: '2026-07-04T11:00:00Z',
},
}),
);
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
expect(await screen.findByText('角色形象 3')).toBeTruthy();
const firstThumbnail = thumbnailElementForLabel('角色形象 1');
const laterThumbnail = thumbnailElementForLabel('角色形象 3');
await waitFor(() => {
expect(observer.isObserved(firstThumbnail)).toBe(true);
expect(observer.isObserved(laterThumbnail)).toBe(true);
});
expect(getAdminAssetReadUrl).not.toHaveBeenCalled();
observer.enter(laterThumbnail);
const laterImage = await screen.findByRole('img', {
name: '素材:角色形象 3',
});
expect(laterImage.getAttribute('src')).toBe(
'https://signed.example.com/generated-character-drafts/editor/spec-3.png',
);
expect(getAdminAssetReadUrl).toHaveBeenCalledTimes(1);
expect(getAdminAssetReadUrl).toHaveBeenLastCalledWith('admin-token', {
objectKey: 'generated-character-drafts/editor/spec-3.png',
expireSeconds: 300,
});
observer.enter(firstThumbnail);
expect(
await screen.findByRole('img', { name: '素材:角色形象 1' }),
).toBeTruthy();
expect(getAdminAssetReadUrl).toHaveBeenCalledTimes(2);
});
test('后台素材查询为大量同时可见的缩略图持续错峰换签', async () => {
vi.useFakeTimers();
const observer = installIntersectionObserverMock();
const entries = Array.from({ length: 105 }, (_, index) =>
generatedAssetAt(index + 1),
);
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries,
nextCursor: null,
});
vi.mocked(getAdminAssetReadUrl).mockImplementation(
async (_token, request) => ({
read: {
objectKey: request.objectKey ?? '',
signedUrl: `https://signed.example.com/${request.objectKey}`,
expiresAt: '2026-07-04T11:00:00Z',
},
}),
);
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await act(async () => {
await Promise.resolve();
});
const thumbnails = entries.map((entry) =>
thumbnailElementForLabel(entry.label),
);
thumbnails.forEach((thumbnail) => {
expect(observer.isObserved(thumbnail)).toBe(true);
observer.enter(thumbnail);
});
await act(async () => {
await Promise.resolve();
});
expect(getAdminAssetReadUrl).not.toHaveBeenCalled();
await act(async () => {
await vi.advanceTimersByTimeAsync(39);
});
expect(getAdminAssetReadUrl).not.toHaveBeenCalled();
await act(async () => {
await vi.advanceTimersByTimeAsync(3_961);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledTimes(100);
await act(async () => {
await vi.advanceTimersByTimeAsync(200);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledTimes(105);
});
test('后台素材查询读取更多后为新进入可视区域的素材换签', async () => {
const observer = installIntersectionObserverMock();
const firstEntry = generatedAssetAt(1);
const nextEntry = generatedAssetAt(81);
vi.mocked(listAdminEditorAssets)
.mockResolvedValueOnce({
entries: [firstEntry],
nextCursor: 'cursor-1',
})
.mockResolvedValueOnce({
entries: [nextEntry],
nextCursor: null,
});
vi.mocked(getAdminAssetReadUrl).mockImplementation(
async (_token, request) => ({
read: {
objectKey: request.objectKey ?? '',
signedUrl: `https://signed.example.com/${request.objectKey}`,
expiresAt: '2026-07-04T11:00:00Z',
},
}),
);
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
fireEvent.click(await screen.findByRole('button', { name: '读取更多' }));
expect(await screen.findByText('角色形象 81')).toBeTruthy();
const nextThumbnail = thumbnailElementForLabel('角色形象 81');
await waitFor(() => expect(observer.isObserved(nextThumbnail)).toBe(true));
expect(getAdminAssetReadUrl).not.toHaveBeenCalled();
observer.enter(nextThumbnail);
expect(
await screen.findByRole('img', { name: '素材:角色形象 81' }),
).toBeTruthy();
expect(getAdminAssetReadUrl).toHaveBeenCalledWith('admin-token', {
objectKey: 'generated-character-drafts/editor/spec-81.png',
expireSeconds: 300,
});
expect(listAdminEditorAssets).toHaveBeenLastCalledWith('admin-token', {
ownerUserId: null,
keyword: null,
createdAfter: null,
createdBefore: null,
limit: 80,
cursor: 'cursor-1',
});
});
test('后台素材查询缩略图换签被限流后有限重试并恢复', async () => {
const observer = installIntersectionObserverMock();
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries: [generatedAsset],
nextCursor: null,
});
vi.mocked(getAdminAssetReadUrl)
.mockRejectedValueOnce({ status: 429 })
.mockResolvedValueOnce({
read: {
objectKey: generatedAsset.objectKey ?? '',
signedUrl:
'https://signed.example.com/generated-character-drafts/editor/spec.png',
expiresAt: '2026-07-04T11:00:00Z',
},
});
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
expect(await screen.findByText('角色形象 1')).toBeTruthy();
const thumbnail = thumbnailElementForLabel('角色形象 1');
await waitFor(() => expect(observer.isObserved(thumbnail)).toBe(true));
observer.enter(thumbnail);
const image = await screen.findByRole(
'img',
{ name: '素材:角色形象 1' },
{ timeout: 2_000 },
);
expect(image.getAttribute('src')).toBe(
'https://signed.example.com/generated-character-drafts/editor/spec.png',
);
expect(getAdminAssetReadUrl).toHaveBeenCalledTimes(2);
});
test('后台素材查询缩略图卸载后停止限流重试', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date());
const observer = installIntersectionObserverMock();
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries: [generatedAsset],
nextCursor: null,
});
vi.mocked(getAdminAssetReadUrl).mockRejectedValue({ status: 429 });
const { unmount } = render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await act(async () => {
await Promise.resolve();
});
const thumbnail = thumbnailElementForLabel('角色形象 1');
expect(observer.isObserved(thumbnail)).toBe(true);
observer.enter(thumbnail);
await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledTimes(1);
unmount();
await act(async () => {
await vi.advanceTimersByTimeAsync(5_000);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledTimes(1);
});
test('后台素材查询将无 objectKey 的绝对 OSS 图片地址换签后展示', async () => {
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries: [
{
...generatedAsset,
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(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
const image = await screen.findByRole('img', { name: '素材:角色形象 1' });
await waitFor(() => {
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,
});
});
test('后台素材查询为独立缩略图路径换签而不误用原图 objectKey', async () => {
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries: [
{
...generatedAsset,
thumbnailSrc: '/generated-character-drafts/editor/spec-thumbnail.png',
},
],
nextCursor: null,
});
vi.mocked(getAdminAssetReadUrl).mockResolvedValue({
read: {
objectKey: 'generated-character-drafts/editor/spec-thumbnail.png',
signedUrl: 'https://signed.example.com/spec-thumbnail.png',
expiresAt: '2026-07-04T11:00:00Z',
},
});
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
const image = await screen.findByRole('img', { name: '素材:角色形象 1' });
expect(image.getAttribute('src')).toBe(
'https://signed.example.com/spec-thumbnail.png',
);
expect(getAdminAssetReadUrl).toHaveBeenCalledWith('admin-token', {
legacyPublicPath: '/generated-character-drafts/editor/spec-thumbnail.png',
expireSeconds: 300,
});
expect(getAdminAssetReadUrl).not.toHaveBeenCalledWith('admin-token', {
objectKey: generatedAsset.objectKey,
expireSeconds: 300,
});
});
test('后台素材查询点击图片缩略图可打开放大预览', async () => {
const user = userEvent.setup();
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await user.click(
await screen.findByRole('button', { name: '素材:角色形象 1' }),
);
const dialog = await screen.findByRole('dialog', { name: '素材预览' });
const image = await within(dialog).findByRole('img', {
name: '图片预览:角色形象 1',
});
await waitFor(() => {
expect(image.getAttribute('src')).toBe(
'https://signed.example.com/generated-character-drafts/editor/spec.png',
);
});
});
test('后台素材查询将角色动画首帧 PNG 作为图片预览', async () => {
const user = userEvent.setup();
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries: [
{
...generatedAsset,
assetId: 'asset-character-animation-1',
label: '角色动作首帧',
imageSrc: '/generated-animations/editor/source-1/task-1/frame00.png',
objectKey: 'generated-animations/editor/source-1/task-1/frame00.png',
assetKind: 'character-animation',
thumbnailSrc:
'/generated-animations/editor/source-1/task-1/frame00.png',
},
],
nextCursor: null,
});
vi.mocked(getAdminAssetReadUrl).mockResolvedValue({
read: {
objectKey: 'generated-animations/editor/source-1/task-1/frame00.png',
signedUrl: 'https://signed.example.com/character-animation-frame00.png',
expiresAt: '2026-07-04T11:00:00Z',
},
});
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await user.click(
await screen.findByRole('button', { name: '素材:角色动作首帧' }),
);
const dialog = await screen.findByRole('dialog', { name: '素材预览' });
const image = await within(dialog).findByRole('img', {
name: '图片预览:角色动作首帧',
});
await waitFor(() => {
expect(image.getAttribute('src')).toBe(
'https://signed.example.com/character-animation-frame00.png',
);
});
expect(within(dialog).queryByLabelText('视频预览:角色动作首帧')).toBeNull();
});
test('后台素材查询音频素材使用统一封面缩略图', async () => {
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries: [
{
...generatedAsset,
assetId: 'asset-audio-1',
label: '胜利音效',
imageSrc: '/generated-editor-audios/sfx.mp3',
objectKey: 'generated-editor-audios/sfx.mp3',
assetKind: 'sound-effect',
},
],
nextCursor: null,
});
render(
<AdminEditorAssetQueryPage 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 () => {
const user = userEvent.setup();
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries: [
{
...generatedAsset,
assetId: 'asset-audio-1',
label: '胜利音效',
imageSrc: '/generated-editor-audios/sfx.mp3',
objectKey: 'generated-editor-audios/sfx.mp3',
assetKind: 'sound-effect',
},
],
nextCursor: null,
});
vi.mocked(getAdminAssetReadUrl).mockResolvedValue({
read: {
objectKey: 'generated-editor-audios/sfx.mp3',
signedUrl: 'https://signed.example.com/generated-editor-audios/sfx.mp3',
expiresAt: '2026-07-04T11:00:00Z',
},
});
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await user.click(
await screen.findByRole('button', { name: '素材:胜利音效' }),
);
const player = await screen.findByLabelText('音频预览:胜利音效');
await waitFor(() => {
expect(player.getAttribute('src')).toBe(
'https://signed.example.com/generated-editor-audios/sfx.mp3',
);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledWith('admin-token', {
objectKey: 'generated-editor-audios/sfx.mp3',
expireSeconds: 300,
});
});
test('后台素材查询视频缩略图使用封面并在预览中播放视频', async () => {
const user = userEvent.setup();
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries: [
{
...generatedAsset,
assetId: 'asset-video-1',
label: '生成视频 1',
imageSrc: '/generated-editor-videos/task-1/preview.mp4',
objectKey:
'generated-character-drafts/editor-videos/task-1/preview.mp4',
assetKind: 'editor_video',
thumbnailSrc: '/generated-editor-videos/task-1/cover.png',
},
],
nextCursor: null,
});
vi.mocked(getAdminAssetReadUrl).mockImplementation(
async (_token, request) => {
if ('legacyPublicPath' in request) {
return {
read: {
objectKey: 'generated-editor-videos/task-1/cover.png',
signedUrl: 'https://signed.example.com/video-cover.png',
expiresAt: '2026-07-04T11:00:00Z',
},
};
}
const objectKey = request.objectKey ?? '';
return {
read: {
objectKey,
signedUrl: objectKey.endsWith('.mp4')
? 'https://signed.example.com/video-preview.mp4'
: 'https://signed.example.com/video-cover-by-key.png',
expiresAt: '2026-07-04T11:00:00Z',
},
};
},
);
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
const image = await screen.findByRole('img', { name: '素材:生成视频 1' });
await waitFor(() => {
expect(image.getAttribute('src')).toBe(
'https://signed.example.com/video-cover.png',
);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledWith('admin-token', {
legacyPublicPath: '/generated-editor-videos/task-1/cover.png',
expireSeconds: 300,
});
expect(getAdminAssetReadUrl).not.toHaveBeenCalledWith('admin-token', {
objectKey: 'generated-character-drafts/editor-videos/task-1/preview.mp4',
expireSeconds: 300,
});
vi.mocked(getAdminAssetReadUrl).mockClear();
await user.click(screen.getByRole('button', { name: '素材:生成视频 1' }));
const video = await screen.findByLabelText('视频预览:生成视频 1');
await waitFor(() => {
expect(video.getAttribute('src')).toBe(
'https://signed.example.com/video-preview.mp4',
);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledWith('admin-token', {
objectKey: 'generated-character-drafts/editor-videos/task-1/preview.mp4',
expireSeconds: 300,
});
});
test('后台素材查询将无 objectKey 的绝对 OSS 视频和封面分别换签', async () => {
const user = userEvent.setup();
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries: [
{
...generatedAsset,
assetId: 'asset-video-2',
label: '生成视频 2',
imageSrc:
'https://genarrative.oss-cn-shanghai.aliyuncs.com/generated-editor-videos/task-2/preview.mp4?versionId=video',
objectKey: null,
assetKind: 'editor_video',
thumbnailSrc:
'https://genarrative.oss-cn-shanghai.aliyuncs.com/generated-editor-videos/task-2/cover.png?versionId=poster',
},
],
nextCursor: null,
});
vi.mocked(getAdminAssetReadUrl).mockImplementation(
async (_token, request) => {
const legacyPublicPath = request.legacyPublicPath ?? '';
return {
read: {
objectKey: legacyPublicPath.replace(/^\//u, ''),
signedUrl: legacyPublicPath.endsWith('/cover.png')
? 'https://signed.example.com/video-2-cover.png'
: 'https://signed.example.com/video-2-preview.mp4',
expiresAt: '2026-07-04T11:00:00Z',
},
};
},
);
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
const thumbnail = await screen.findByRole('img', {
name: '素材:生成视频 2',
});
await waitFor(() => {
expect(thumbnail.getAttribute('src')).toBe(
'https://signed.example.com/video-2-cover.png',
);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledWith('admin-token', {
legacyPublicPath: '/generated-editor-videos/task-2/cover.png',
expireSeconds: 300,
});
vi.mocked(getAdminAssetReadUrl).mockClear();
await user.click(screen.getByRole('button', { name: '素材:生成视频 2' }));
const video = await screen.findByLabelText('视频预览:生成视频 2');
await waitFor(() => {
expect(video.getAttribute('src')).toBe(
'https://signed.example.com/video-2-preview.mp4',
);
expect(video.getAttribute('poster')).toBe(
'https://signed.example.com/video-2-cover.png',
);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledWith('admin-token', {
legacyPublicPath: '/generated-editor-videos/task-2/preview.mp4',
expireSeconds: 300,
});
expect(getAdminAssetReadUrl).toHaveBeenCalledWith('admin-token', {
legacyPublicPath: '/generated-editor-videos/task-2/cover.png',
expireSeconds: 300,
});
});
test('后台素材查询格式化微秒时间文本', async () => {
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries: [
{
...generatedAsset,
createdAt: '1783231493.573727Z',
updatedAt: '1783231493.573727Z',
},
],
nextCursor: null,
});
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
expect(await screen.findByText('角色形象 1')).toBeTruthy();
expect(screen.queryByText('1783231493.573727Z')).toBeNull();
});
test('后台素材查询可查看素材详情', async () => {
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
fireEvent.click(await screen.findByRole('button', { name: '详情' }));
const dialog = screen.getByRole('dialog', { name: '素材详情' });
expect(within(dialog).getByText('asset-1')).toBeTruthy();
expect(within(dialog).getByText('1024 x 1024')).toBeTruthy();
expect(within(dialog).getByText('12 泥点')).toBeTruthy();
expect(within(dialog).getByText(/"style": "clay"/u)).toBeTruthy();
});
test('后台素材查询可打开弹窗查看完整提示词', async () => {
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
fireEvent.click(
await screen.findByRole('button', { name: '完整提示词内容' }),
);
const dialog = screen.getByRole('dialog', { name: '完整提示词' });
expect(within(dialog).getByText('完整提示词内容')).toBeTruthy();
fireEvent.click(
within(dialog).getByRole('button', { name: '关闭完整提示词' }),
);
await waitFor(() => {
expect(screen.queryByRole('dialog', { name: '完整提示词' })).toBeNull();
});
});