diff --git a/apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx b/apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx index 3ca7ab6be..434373f2d 100644 --- a/apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx +++ b/apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx @@ -1,6 +1,8 @@ /* @vitest-environment jsdom */ +import { PLATFORM_GAME_LOADING_TIMEOUT_MS } from '@genarrative/shared/components'; import { + act, fireEvent, render, screen, @@ -317,3 +319,99 @@ test('详情展示发布者和冻结资料,并试玩当前待审版本', async ), ); }); + +/** 打开详情并点开试玩,返回试玩 iframe。 */ +async function openPendingVersionPreview() { + render( + , + ); + await screen.findByText(gameTitle); + fireEvent.click(screen.getByRole('button', { name: '详情' })); + await screen.findAllByText(/测试作者/u); + fireEvent.click(screen.getByRole('button', { name: '试玩当前待审版本' })); + return screen.findByTitle('测试游戏 待审版本试玩'); +} + +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', + }); + + const frame = await openPendingVersionPreview(); + + // 加载期不能只留一片空白:审核员要看到正在加载哪个游戏。 + const loadingSurface = screen.getByRole('status'); + expect(loadingSurface.textContent).toContain('测试游戏'); + expect( + loadingSurface.classList.contains('platform-game-loading--panel'), + ).toBe(true); + + fireEvent.load(frame); + await waitFor(() => expect(screen.queryByRole('status')).toBeNull()); +}); + +test('试玩加载超过上限后给出超时说明与重建会话入口', async () => { + vi.mocked(getAdminGameDistributionVersion).mockResolvedValue(detailResponse); + vi.mocked(createAdminGameDistributionPreviewSession) + .mockResolvedValueOnce({ + previewUrl: + 'http://127.0.0.1:8082/api/game-distribution/admin-previews/preview-1/', + expiresAt: '2026-10-02T12:10:00Z', + versionId: 'version-1', + }) + .mockResolvedValueOnce({ + previewUrl: + 'http://127.0.0.1:8082/api/game-distribution/admin-previews/preview-2/', + expiresAt: '2026-10-02T12:20:00Z', + versionId: 'version-1', + }); + + render( + , + ); + await screen.findByText(gameTitle); + fireEvent.click(screen.getByRole('button', { name: '详情' })); + await screen.findAllByText(/测试作者/u); + + // 试玩会话与超时定时器都必须落在假定时器窗口内,才能按上限推进等待。 + vi.useFakeTimers(); + try { + fireEvent.click(screen.getByRole('button', { name: '试玩当前待审版本' })); + await act(async () => { + await Promise.resolve(); + }); + expect(screen.getByTitle('测试游戏 待审版本试玩')).toBeTruthy(); + expect(screen.queryByText('试玩版本加载超时')).toBeNull(); + + await act(async () => { + vi.advanceTimersByTime(PLATFORM_GAME_LOADING_TIMEOUT_MS); + }); + expect(screen.getByRole('status').textContent).toContain( + '试玩版本加载超时', + ); + + fireEvent.click(screen.getByRole('button', { name: '重新创建试玩会话' })); + await act(async () => { + await Promise.resolve(); + }); + expect(createAdminGameDistributionPreviewSession).toHaveBeenCalledTimes(2); + // 新会话必须回到加载态,不能沿用上一个会话的「已就绪」或超时结论。 + const rebuilt = screen.getByTitle('测试游戏 待审版本试玩'); + expect(rebuilt.getAttribute('src')).toBe( + 'http://127.0.0.1:8082/api/game-distribution/admin-previews/preview-2/', + ); + expect(screen.queryByText('试玩版本加载超时')).toBeNull(); + } finally { + vi.useRealTimers(); + } +}); diff --git a/apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx b/apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx index f63a9c79e..158e5381e 100644 --- a/apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx +++ b/apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx @@ -10,6 +10,8 @@ import { formatAdminDateTime, GameDetailDisplay, Modal, + PLATFORM_GAME_LOADING_TIMEOUT_MS, + PlatformGameLoadingSurface, TextField, } from '@genarrative/shared/components'; import { RefreshCcw } from 'lucide-react'; @@ -591,6 +593,20 @@ function AdminGameDistributionReviewDetailView({ onReject: () => void; onApprove: () => void; }) { + // 试玩会话是按 version 现建的:新会话必须回到加载态,不能沿用上一个会话的 ready。 + const [isPreviewFrameReady, setIsPreviewFrameReady] = useState(false); + const [isPreviewTimedOut, setIsPreviewTimedOut] = useState(false); + + useEffect(() => { + setIsPreviewFrameReady(false); + setIsPreviewTimedOut(false); + if (!previewUrl) return undefined; + const timer = window.setTimeout(() => { + setIsPreviewTimedOut(true); + }, PLATFORM_GAME_LOADING_TIMEOUT_MS); + return () => window.clearTimeout(timer); + }, [previewUrl]); + if (!detail) { return ( @@ -606,6 +622,10 @@ function AdminGameDistributionReviewDetailView({ ); } + const coverUrl = detail.game.coverObjectKey + ? assetUrls[detail.game.coverObjectKey] + : ''; + return ( {previewError} ) : null} {previewUrl ? ( -