Files
Genarrative/apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx
T
suzmii 072284b846 后台游戏管理与审核页排版修复,安全下架入口收敛到游戏管理页
- 游戏管理页:下架改为独立面板(原因必填、校验提示、仍走二次确认),操作列与作者列排版调整,gameId 省略号加 title
- 游戏管理页测试:拆成空原因不提交、面板取消不请求、面板确认后仍需二次确认三条
- 审核页:移除行内游戏级安全下架及其状态与二次确认接线,理由弹窗只服务拒绝,gameId 截断加 title
- 审核页测试:删除两条安全下架用例,替换为行内无该动作、microsZ 时间列渲染、窄视口 gameId 截断
- 新增 adminGameDistributionReview.css:审核页专属列宽与动作列 nowrap,含 1366/980 两档断点
- admin-web admin.css:新增游戏管理表格 5 条排版规则
- shared AdminFormat:新增 normalizeAdminTimestamp,兼容后端 seconds.microsZ 时间口径
- shared AdminFormat 测试:新增 5 条覆盖微秒口径、补零、ISO 与 epoch 兼容、非法值回落
- shared admin.css:admin-status 改为 nowrap
- 文档:后台游戏审核详情计划加取代标注,说明审核页只承载版本级动作
2026-10-04 00:17:21 +08:00

281 lines
9.1 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,
listAdminGameDistributionReviews,
reviewAdminGameDistributionVersion,
} from '../api/adminApiClient';
import type {
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(),
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',
};
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(reviewAdminGameDistributionVersion).mockReset();
vi.mocked(getAdminGameDistributionVersion).mockReset();
vi.mocked(createAdminGameDistributionPreviewSession).mockReset();
vi.mocked(listAdminGameDistributionReviews).mockResolvedValue({
entries: [entry],
nextCursor: null,
});
});
test('通过审核只提交当前 publicationRevision 并刷新列表', async () => {
vi.mocked(reviewAdminGameDistributionVersion).mockResolvedValue({
version: { ...entry, status: 'published' },
replayed: false,
});
render(
<AdminGameDistributionReviewPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
await screen.findByText('game_1');
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('game_1');
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('game_1');
// 「安全下架」是游戏级动作(强制下线线上版本),入口在「游戏管理」页;
// 审核一个新版本时不该出现该动作。
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('game_1');
const row = screen.getByText('game_1').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('game_1');
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 截断显示,完整值由 title 暴露', async () => {
render(
<AdminGameDistributionReviewPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
await screen.findByText('game_1');
const gameIdCell = screen.getByTitle('game_1');
expect(gameIdCell.textContent).toBe('game_1');
});
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('game_1');
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,
}),
),
);
});