620ac52461
- AdminGameDistributionReviewPage.tsx:待审列表只回版本字段,新增 resolveGameMeta 用既有后台作品列表接口按 gameId 反查标题/作者,逐游戏去重并按会话缓存;单个游戏取不到只回落显示 gameId,不阻断审核列表 - 同文件:loadReviews 在拿到 entries 后并行补齐本页 distinct gameId 的元数据 - 同文件:游戏列由单个 gameId 文本改为「游戏名(主行)+ 作者名与 user_id(副行,截断显示,完整值放 title)」 - adminGameDistributionReview.css:以 .admin-game-review-game 网格布局替换 .admin-game-review-game-id,保留窄视口截断与 title 出口 - AdminGameDistributionReviewPage.test.tsx:补 listAdminGameDistributionGames 的 mock 与游戏列表 fixture,断言由 gameId 改为游戏名
320 lines
11 KiB
TypeScript
320 lines
11 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import {
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
within,
|
|
} from '@testing-library/react';
|
|
import { beforeEach, expect, test, vi } from 'vitest';
|
|
|
|
import {
|
|
createAdminGameDistributionPreviewSession,
|
|
getAdminGameDistributionVersion,
|
|
listAdminGameDistributionGames,
|
|
listAdminGameDistributionReviews,
|
|
reviewAdminGameDistributionVersion,
|
|
} from '../api/adminApiClient';
|
|
import type {
|
|
AdminGameDistributionGameListResponse,
|
|
AdminGameDistributionReviewEntry,
|
|
AdminGameDistributionVersionDetailResponse,
|
|
} from '../api/adminApiTypes';
|
|
import { AdminGameDistributionReviewPage } from './AdminGameDistributionReviewPage';
|
|
|
|
vi.mock('../api/adminApiClient', () => ({
|
|
isAdminApiError: vi.fn(
|
|
(error: unknown) =>
|
|
typeof error === 'object' &&
|
|
error !== null &&
|
|
'status' in error &&
|
|
typeof error.status === 'number',
|
|
),
|
|
formatAdminApiError: vi.fn((error: unknown) =>
|
|
error instanceof Error ? error.message : '请求失败',
|
|
),
|
|
createAdminGameDistributionPreviewSession: vi.fn(),
|
|
getAdminGameDistributionVersion: vi.fn(),
|
|
listAdminGameDistributionGames: vi.fn(),
|
|
listAdminGameDistributionReviews: vi.fn(),
|
|
reviewAdminGameDistributionVersion: vi.fn(),
|
|
}));
|
|
const entry: AdminGameDistributionReviewEntry = {
|
|
versionId: 'version-1',
|
|
gameId: 'game_1',
|
|
versionNumber: 2,
|
|
packageSha256: 'a'.repeat(64),
|
|
packageBytes: 2048,
|
|
status: 'pending_review',
|
|
publicationRevision: 4,
|
|
reviewReason: null,
|
|
// 真实后台下发的是 `seconds.microsZ`(非 ISO 8601),见 shared-kernel `format_timestamp_micros`。
|
|
createdAt: '1791008678.817257Z',
|
|
updatedAt: '1791008678.817257Z',
|
|
};
|
|
|
|
/** 游戏名与作者由既有后台作品列表接口按 gameId 反查(待审列表本身不含这两个字段)。 */
|
|
const gameTitle = '测试游戏名';
|
|
const gameListResponse: AdminGameDistributionGameListResponse = {
|
|
games: [
|
|
{
|
|
gameId: 'game_1',
|
|
title: gameTitle,
|
|
author: { id: 'user-9f1', name: '136****8867', avatarUrl: null },
|
|
status: 'unpublished',
|
|
versionCount: 1,
|
|
playCount: 0,
|
|
activeVersionId: null,
|
|
publicationRevision: 4,
|
|
createdAt: '2026-09-20T08:00:00Z',
|
|
updatedAt: '2026-09-20T08:00:00Z',
|
|
deletedAt: null,
|
|
versions: [],
|
|
},
|
|
],
|
|
nextCursor: null,
|
|
};
|
|
|
|
const detailResponse: AdminGameDistributionVersionDetailResponse = {
|
|
game: {
|
|
id: 'game_1',
|
|
title: '测试游戏',
|
|
summary: '测试简介',
|
|
description: '测试详细介绍',
|
|
category: '益智',
|
|
tags: ['测试'],
|
|
coverObjectKey: null,
|
|
screenshots: [],
|
|
author: { id: 'user-1', name: '测试作者', avatarUrl: null },
|
|
deviceSupport: { desktop: true, mobile: false, touch: true },
|
|
inputModes: ['keyboard'],
|
|
orientation: 'landscape',
|
|
status: 'unpublished',
|
|
publicationRevision: 7,
|
|
playCount: 0,
|
|
createdAt: '2026-09-20T08:00:00Z',
|
|
},
|
|
version: {
|
|
...entry,
|
|
packageFileCount: 3,
|
|
entryUrl: null,
|
|
recoveryAction: 'wait',
|
|
frozenMetadata: { title: '测试游戏' },
|
|
},
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(listAdminGameDistributionReviews).mockReset();
|
|
vi.mocked(listAdminGameDistributionGames).mockReset();
|
|
vi.mocked(reviewAdminGameDistributionVersion).mockReset();
|
|
vi.mocked(getAdminGameDistributionVersion).mockReset();
|
|
vi.mocked(createAdminGameDistributionPreviewSession).mockReset();
|
|
vi.mocked(listAdminGameDistributionReviews).mockResolvedValue({
|
|
entries: [entry],
|
|
nextCursor: null,
|
|
});
|
|
vi.mocked(listAdminGameDistributionGames).mockResolvedValue(gameListResponse);
|
|
});
|
|
|
|
test('通过审核只提交当前 publicationRevision 并刷新列表', async () => {
|
|
vi.mocked(reviewAdminGameDistributionVersion).mockResolvedValue({
|
|
version: { ...entry, status: 'published' },
|
|
replayed: false,
|
|
});
|
|
|
|
render(
|
|
<AdminGameDistributionReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
await screen.findByText(gameTitle);
|
|
|
|
expect(screen.queryByLabelText('发行入口')).toBeNull();
|
|
expect(screen.queryByText('通过后由系统分配发行地址')).toBeNull();
|
|
expect(screen.queryByLabelText('拒绝理由')).toBeNull();
|
|
expect(screen.queryByLabelText('下架原因')).toBeNull();
|
|
fireEvent.click(screen.getByRole('button', { name: '通过' }));
|
|
|
|
await waitFor(() =>
|
|
expect(reviewAdminGameDistributionVersion).toHaveBeenCalledTimes(1),
|
|
);
|
|
const [token, versionId, idempotencyKey, payload] =
|
|
vi.mocked(reviewAdminGameDistributionVersion).mock.calls[0] ?? [];
|
|
expect(token).toBe('admin-token');
|
|
expect(versionId).toBe('version-1');
|
|
expect(String(idempotencyKey)).toContain('version-1');
|
|
expect(payload).toEqual({
|
|
decision: 'approve',
|
|
expectedPublicationRevision: 4,
|
|
});
|
|
await waitFor(() =>
|
|
expect(vi.mocked(listAdminGameDistributionReviews)).toHaveBeenCalledTimes(
|
|
2,
|
|
),
|
|
);
|
|
});
|
|
|
|
test('点击拒绝后填写理由再提交审核接口', async () => {
|
|
vi.mocked(reviewAdminGameDistributionVersion).mockResolvedValue({
|
|
version: { ...entry, status: 'rejected', reviewReason: '运行时报错' },
|
|
replayed: false,
|
|
});
|
|
|
|
render(
|
|
<AdminGameDistributionReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
await screen.findByText(gameTitle);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '拒绝' }));
|
|
const dialog = await screen.findByRole('dialog');
|
|
const reasonInput = within(dialog).getByRole('textbox', {
|
|
name: '拒绝理由',
|
|
});
|
|
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '确认拒绝' }));
|
|
expect(await within(dialog).findByText('拒绝审核必须填写理由')).toBeTruthy();
|
|
expect(reviewAdminGameDistributionVersion).not.toHaveBeenCalled();
|
|
|
|
fireEvent.change(reasonInput, { target: { value: '运行时报错' } });
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '确认拒绝' }));
|
|
|
|
await waitFor(() =>
|
|
expect(reviewAdminGameDistributionVersion).toHaveBeenCalledTimes(1),
|
|
);
|
|
const [, versionId, , payload] =
|
|
vi.mocked(reviewAdminGameDistributionVersion).mock.calls[0] ?? [];
|
|
expect(versionId).toBe('version-1');
|
|
expect(payload).toEqual({
|
|
decision: 'reject',
|
|
expectedPublicationRevision: 4,
|
|
reviewReason: '运行时报错',
|
|
});
|
|
});
|
|
|
|
test('待审版本行内只保留版本级审核动作,不提供游戏级安全下架', async () => {
|
|
render(
|
|
<AdminGameDistributionReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
await screen.findByText(gameTitle);
|
|
|
|
// 「安全下架」是游戏级动作(强制下线线上版本),入口在「游戏管理」页;
|
|
// 审核一个新版本时不该出现该动作。
|
|
expect(screen.queryByRole('button', { name: '安全下架' })).toBeNull();
|
|
expect(screen.queryByText('继续下架')).toBeNull();
|
|
expect(screen.queryByLabelText('下架原因')).toBeNull();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '拒绝' }));
|
|
const dialog = await screen.findByRole('dialog');
|
|
expect(within(dialog).queryByRole('button', { name: '继续下架' })).toBeNull();
|
|
expect(within(dialog).getByLabelText('拒绝理由')).toBeTruthy();
|
|
});
|
|
|
|
test('提交时间列把后端的 seconds.microsZ 渲染成可读本地时间', async () => {
|
|
render(
|
|
<AdminGameDistributionReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
await screen.findByText(gameTitle);
|
|
|
|
const row = screen.getByText(gameTitle).closest('tr');
|
|
expect(row).not.toBeNull();
|
|
const submittedCell = within(row as HTMLElement).getAllByRole('cell')[3];
|
|
// 解析不出来时会回落到 `-`,这正是本次修复前的线上表现。
|
|
expect(submittedCell?.textContent).not.toBe('-');
|
|
expect(submittedCell?.textContent).toMatch(
|
|
/^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/u,
|
|
);
|
|
});
|
|
|
|
test('取消理由输入时不做审核操作', async () => {
|
|
render(
|
|
<AdminGameDistributionReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
await screen.findByText(gameTitle);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '拒绝' }));
|
|
const dialog = await screen.findByRole('dialog');
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '取消' }));
|
|
|
|
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
expect(reviewAdminGameDistributionVersion).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('游戏列显示游戏名与作者,不再把 gameId 当主展示', async () => {
|
|
render(
|
|
<AdminGameDistributionReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
await screen.findByText(gameTitle);
|
|
|
|
// 按 gameId 反查一次,第二次刷新走缓存(本次只加载一次列表)。
|
|
expect(listAdminGameDistributionGames).toHaveBeenCalledWith(
|
|
'admin-token',
|
|
expect.objectContaining({ keyword: 'game_1' }),
|
|
);
|
|
|
|
const row = screen.getByText(gameTitle).closest('tr');
|
|
expect(row).not.toBeNull();
|
|
const gameCell = within(row as HTMLElement).getAllByRole('cell')[0];
|
|
// 主展示是游戏名 + 打码作者名;gameId 只留在悬浮标题里。
|
|
expect(gameCell?.textContent).toContain('136****8867');
|
|
expect(gameCell?.textContent).not.toContain('game_1');
|
|
expect(screen.getByTitle('user-9f1').textContent).toBe('user-9f1');
|
|
expect(gameCell?.querySelector('[title="game_1"]')).not.toBeNull();
|
|
});
|
|
|
|
test('详情展示发布者和冻结资料,并试玩当前待审版本', async () => {
|
|
vi.mocked(getAdminGameDistributionVersion).mockResolvedValue(detailResponse);
|
|
vi.mocked(createAdminGameDistributionPreviewSession).mockResolvedValue({
|
|
previewUrl:
|
|
'http://127.0.0.1:8082/api/game-distribution/admin-previews/preview-1/',
|
|
expiresAt: '2026-10-02T12:10:00Z',
|
|
versionId: 'version-1',
|
|
});
|
|
|
|
render(
|
|
<AdminGameDistributionReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
await screen.findByText(gameTitle);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '详情' }));
|
|
expect((await screen.findAllByText(/测试作者/u)).length).toBeGreaterThan(0);
|
|
expect(screen.getByText('测试详细介绍')).toBeTruthy();
|
|
fireEvent.click(screen.getByRole('button', { name: '试玩当前待审版本' }));
|
|
const frame = await screen.findByTitle('测试游戏 待审版本试玩');
|
|
expect(frame.getAttribute('src')).toBe(
|
|
'http://127.0.0.1:8082/api/game-distribution/admin-previews/preview-1/',
|
|
);
|
|
expect(frame.getAttribute('sandbox')).toBe('allow-scripts');
|
|
fireEvent.click(screen.getByRole('button', { name: '通过' }));
|
|
await waitFor(() =>
|
|
expect(reviewAdminGameDistributionVersion).toHaveBeenCalledWith(
|
|
'admin-token',
|
|
'version-1',
|
|
expect.any(String),
|
|
expect.objectContaining({
|
|
decision: 'approve',
|
|
expectedPublicationRevision: 7,
|
|
}),
|
|
),
|
|
);
|
|
});
|