Merge remote-tracking branch 'origin/master' into feat/pricing-plan
Project CI / AI game creator shell Rust crates (pull_request) Successful in 6m0s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m43s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m42s
Project CI / Frontend tests (pull_request) Failing after 1m16s
Project CI / Backend tests (pull_request) Successful in 8m51s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m34s
Project CI / Repository checks (pull_request) Successful in 5m34s
Project CI / Native shell tests (pull_request) Successful in 7m18s

# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
This commit is contained in:
2026-10-06 10:14:15 +08:00
131 changed files with 15731 additions and 5835 deletions
@@ -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(
<AdminGameDistributionReviewPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
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(
<AdminGameDistributionReviewPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
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();
}
});
@@ -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 (
<AdminPage wide>
@@ -606,6 +622,10 @@ function AdminGameDistributionReviewDetailView({
);
}
const coverUrl = detail.game.coverObjectKey
? assetUrls[detail.game.coverObjectKey]
: '';
return (
<AdminPage wide>
<AdminPageHeading
@@ -682,13 +702,46 @@ function AdminGameDistributionReviewDetailView({
<AdminAlert tone="warning">{previewError}</AdminAlert>
) : null}
{previewUrl ? (
<iframe
title={`${detail.game.title} 待审版本试玩`}
src={previewUrl}
sandbox="allow-scripts"
referrerPolicy="no-referrer"
className="admin-game-review-preview"
/>
<div className="admin-game-review-preview-shell">
{isPreviewFrameReady ? null : (
<PlatformGameLoadingSurface
tone="panel"
title={detail.game.title}
cover={
coverUrl ? (
<img alt={`${detail.game.title} 封面`} src={coverUrl} />
) : null
}
message={isPreviewTimedOut ? '试玩版本加载超时' : undefined}
hint={
isPreviewTimedOut
? '可以重新创建试玩会话,或稍后再试。'
: undefined
}
stalled={isPreviewTimedOut}
/>
)}
<iframe
title={`${detail.game.title} 待审版本试玩`}
src={previewUrl}
sandbox="allow-scripts"
referrerPolicy="no-referrer"
className="admin-game-review-preview"
onLoad={() => setIsPreviewFrameReady(true)}
/>
</div>
) : null}
{previewUrl && isPreviewTimedOut && !isPreviewFrameReady ? (
<AdminActionRow className="admin-review-actions">
<AdminButton
variant="secondary"
type="button"
disabled={isPreviewLoading}
onClick={onPreview}
>
{isPreviewLoading ? '准备试玩…' : '重新创建试玩会话'}
</AdminButton>
</AdminActionRow>
) : null}
</section>
</GameDetailDisplay>
+13 -1
View File
@@ -3590,16 +3590,28 @@ button:disabled {
color: var(--platform-text-strong);
}
.admin-game-review-preview-shell {
/* 加载面(PlatformGameLoadingSurface)按绝对定位铺在预览区上,外壳必须是定位祖先。 */
position: relative;
margin-top: 1rem;
min-height: 42rem;
overflow: hidden;
border-radius: 1rem;
}
.admin-game-review-preview {
display: block;
width: 100%;
min-height: 42rem;
margin-top: 1rem;
border: 1px solid var(--platform-surface-border);
border-radius: 1rem;
background: #fff;
}
.admin-game-review-preview-shell .platform-game-loading {
border-radius: 1rem;
}
/*
* 游戏管理列表(AdminGameManagementPage)专属排布,选择器全部框在本页表格内,
* 不影响其它后台页面: