/* @vitest-environment jsdom */ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, expect, test, vi } from 'vitest'; import { listAdminGameDistributionReviews, reviewAdminGameDistributionVersion, suspendAdminGameDistributionGame, } from '../api/adminApiClient'; import type { AdminGameDistributionReviewEntry } from '../api/adminApiTypes'; import { AdminGameDistributionReviewPage, resolveGameReleaseEntryUrlError, } 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 : '请求失败', ), listAdminGameDistributionReviews: vi.fn(), reviewAdminGameDistributionVersion: vi.fn(), suspendAdminGameDistributionGame: 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, createdAt: '2026-09-20T08:00:00Z', updatedAt: '2026-09-20T08:00:00Z', }; beforeEach(() => { vi.mocked(listAdminGameDistributionReviews).mockReset(); vi.mocked(reviewAdminGameDistributionVersion).mockReset(); vi.mocked(suspendAdminGameDistributionGame).mockReset(); vi.mocked(listAdminGameDistributionReviews).mockResolvedValue({ entries: [entry], nextCursor: null, }); }); test('发行入口必须是带完整来源的 HTTPS 地址', () => { expect(resolveGameReleaseEntryUrlError('')).toBe('请填写发行入口'); expect( resolveGameReleaseEntryUrlError('http://games.test/a/index.html'), ).toBe('发行入口必须以 https:// 开头'); expect( resolveGameReleaseEntryUrlError('https://games.test/a/index.html?token=1'), ).toBe('发行入口不能包含 query 或 fragment'); expect( resolveGameReleaseEntryUrlError('https://u:p@games.test/a/index.html'), ).toBe('发行入口不能包含凭据'); expect( resolveGameReleaseEntryUrlError('https://games.test/a/index.html'), ).toBe(''); }); test('通过审核时提交当前 publicationRevision 与发行入口并刷新列表', async () => { vi.mocked(reviewAdminGameDistributionVersion).mockResolvedValue({ version: { ...entry, status: 'published' }, replayed: false, }); render( , ); await screen.findByText('game_1'); fireEvent.change(screen.getByLabelText('发行入口'), { target: { value: 'https://games.test/releases/game_1/index.html' }, }); 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, entryUrl: 'https://games.test/releases/game_1/index.html', }); await waitFor(() => expect(vi.mocked(listAdminGameDistributionReviews)).toHaveBeenCalledTimes( 2, ), ); }); test('缺少拒绝理由时不调用审核接口', async () => { render( , ); await screen.findByText('game_1'); fireEvent.click(screen.getByRole('button', { name: '拒绝' })); expect(await screen.findByText('拒绝审核必须填写理由')).toBeTruthy(); expect(reviewAdminGameDistributionVersion).not.toHaveBeenCalled(); }); test('安全下架需要二次确认,并携带公开修订号与原因', async () => { vi.mocked(suspendAdminGameDistributionGame).mockResolvedValue({ game: { id: 'game_1', title: '测试游戏', status: 'suspended', publicationRevision: 5, }, replayed: false, }); render( , ); await screen.findByText('game_1'); fireEvent.change(screen.getByLabelText('下架原因'), { target: { value: '盗用素材' }, }); fireEvent.click(screen.getByRole('button', { name: '安全下架' })); // 第一次点击只弹出确认面板,不直接调用后端。 expect(suspendAdminGameDistributionGame).not.toHaveBeenCalled(); expect(await screen.findByRole('dialog')).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: '确认' })); await waitFor(() => expect(suspendAdminGameDistributionGame).toHaveBeenCalledTimes(1), ); const [token, gameId, idempotencyKey, payload] = vi.mocked(suspendAdminGameDistributionGame).mock.calls[0] ?? []; expect(token).toBe('admin-token'); expect(gameId).toBe('game_1'); expect(String(idempotencyKey)).toContain('game_1'); expect(payload).toEqual({ expectedPublicationRevision: 4, reason: '盗用素材', }); expect(await screen.findByText(/已安全下架/u)).toBeTruthy(); }); test('取消确认时不下架', async () => { render( , ); await screen.findByText('game_1'); fireEvent.click(screen.getByRole('button', { name: '安全下架' })); await screen.findByRole('dialog'); fireEvent.click(screen.getByRole('button', { name: '取消' })); await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()); expect(suspendAdminGameDistributionGame).not.toHaveBeenCalled(); });