修复在线游玩加载体验:加载期黑屏/空白与发行资源传输开销
- packages/shared 新增共享游戏加载面 PlatformGameLoadingSurface:封面/标题 + 动效进度 + 按时长推进的阶段文案(准备/资源/引擎)+ 慢加载提示,附用例 - 网页游玩页用共享加载面替换近黑壳上的一行小字,加载上限改为共享常量 PLATFORM_GAME_LOADING_TIMEOUT_MS - 后台审核试玩补加载面、20 秒超时与「重新创建试玩会话」入口,新会话重置加载态;补两条用例 - 发行网关(api-server)对文本类发行资源按 Accept-Encoding 下发 gzip,并下发强 ETag、命中 If-None-Match 返回 304;补 5 条 Rust 用例 - E2E 脚本 check-game-distribution-ops-rollback-e2e 补压缩/Vary/ETag/304/不接受 gzip 四条断言 - 同步 pitfalls、开发运维文档、nginx README 与两份里程碑记录
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
@@ -9,6 +10,8 @@ import {
|
||||
} from '@testing-library/react';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import { PLATFORM_GAME_LOADING_TIMEOUT_MS } from '@genarrative/shared/components';
|
||||
|
||||
import {
|
||||
createAdminGameDistributionPreviewSession,
|
||||
getAdminGameDistributionVersion,
|
||||
@@ -317,3 +320,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,45 @@ 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
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
|
||||
@@ -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)专属排布,选择器全部框在本页表格内,
|
||||
* 不影响其它后台页面:
|
||||
|
||||
Reference in New Issue
Block a user