恢复游戏分发完整实现(特性分支)
- 主站:游戏广场、详情、在线游玩、网页发布与作者中心,以及共享契约与客户端服务 - 后端:module-game-distribution 领域层、SpacetimeDB 表/迁移/绑定、spacetime-client facade、api-server 路由与发行网关 - 后台:游戏审核页(待审列表、通过/拒绝、安全下架) - AGC:发布面板、本地导出包读取命令与发布服务,含默认跳过的真实链路测试 - 运维:发行来源 nginx 模板与门禁、game-distribution:publish 灰度发布开关、OSS PutObject 受控重试 - 文档:主规范、里程碑与实施计划、决策日志与踩坑记录
This commit is contained in:
@@ -43,6 +43,21 @@ const profileCenterMock = vi.hoisted(() => ({
|
||||
rechargeCenter: null as { walletBalance: number } | null,
|
||||
}));
|
||||
|
||||
const gameDistributionMock = vi.hoisted(() => ({
|
||||
cancelGameVersion: vi.fn(),
|
||||
createGame: vi.fn(),
|
||||
createGameVersion: vi.fn(),
|
||||
getGame: vi.fn(),
|
||||
getGameVersion: vi.fn(),
|
||||
listGames: vi.fn(),
|
||||
listMyGames: vi.fn(),
|
||||
submitGameVersion: vi.fn(),
|
||||
unpublishGame: vi.fn(),
|
||||
uploadGamePackage: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/gameDistributionClient', () => gameDistributionMock);
|
||||
|
||||
vi.mock('../auth/AuthUiContext', () => ({
|
||||
useAuthUi: () => authUiMock.value,
|
||||
}));
|
||||
@@ -187,6 +202,10 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
profileCenterMock.rechargeCenter = null;
|
||||
authUiMock.value.user = null;
|
||||
authUiMock.value.canAccessProtectedData = false;
|
||||
window.history.replaceState(null, '', '/creation');
|
||||
for (const mock of Object.values(gameDistributionMock)) {
|
||||
mock.mockReset();
|
||||
}
|
||||
authUiMock.value.openLoginModal.mockReset();
|
||||
responsiveMock.isDesktopLayout = true;
|
||||
});
|
||||
@@ -417,7 +436,7 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
within(navigation)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.getAttribute('aria-label')),
|
||||
).toEqual(['创作', '项目', '我的']);
|
||||
).toEqual(['创作', '项目', '游戏', '我的']);
|
||||
expect(
|
||||
within(navigation)
|
||||
.getByRole('button', { name: '创作' })
|
||||
@@ -466,7 +485,7 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
).toBe('page');
|
||||
});
|
||||
|
||||
it('keeps only profile reachable from the mobile dock', async () => {
|
||||
it('keeps only games and profile reachable from the mobile dock', async () => {
|
||||
responsiveMock.isDesktopLayout = false;
|
||||
const setSelectionStage = vi.fn();
|
||||
render(
|
||||
@@ -483,7 +502,7 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
within(navigation)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.getAttribute('aria-label')),
|
||||
).toEqual(['我的']);
|
||||
).toEqual(['游戏', '我的']);
|
||||
expect(await screen.findByRole('main', { name: '我的' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '下载客户端' })).toBeTruthy();
|
||||
expect(
|
||||
@@ -568,7 +587,7 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
within(navigation)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.getAttribute('aria-label')),
|
||||
).toEqual(['我的']);
|
||||
).toEqual(['游戏', '我的']);
|
||||
expect(
|
||||
within(navigation)
|
||||
.getByRole('button', { name: '我的' })
|
||||
@@ -653,6 +672,146 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('在我的游戏里进入发布新版本时沿用同一个 gameId', async () => {
|
||||
authUiMock.value.user = {
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
displayName: '测试用户',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
};
|
||||
authUiMock.value.canAccessProtectedData = true;
|
||||
gameDistributionMock.listMyGames.mockResolvedValue([
|
||||
{
|
||||
id: 'game-1',
|
||||
title: '星轨防线',
|
||||
summary: '守住轨道城',
|
||||
description: '旧版资料',
|
||||
category: '动作',
|
||||
tags: ['塔防'],
|
||||
coverColor: '#d77a51',
|
||||
icon: '✦',
|
||||
author: { id: 'user-1', name: '测试用户' },
|
||||
deviceSupport: { desktop: true, mobile: false, touch: false },
|
||||
status: 'published',
|
||||
publicationRevision: 3,
|
||||
currentVersion: null,
|
||||
playCount: 1,
|
||||
createdAt: '2026-09-20T08:00:00Z',
|
||||
latestVersion: {
|
||||
versionId: 'gamever-2',
|
||||
gameId: 'game-1',
|
||||
versionNumber: 2,
|
||||
packageSha256: 'e'.repeat(64),
|
||||
packageBytes: 1024,
|
||||
status: 'published',
|
||||
publicationRevision: 3,
|
||||
reviewReason: null,
|
||||
createdAt: '2026-09-20T08:00:00Z',
|
||||
updatedAt: '2026-09-20T09:00:00Z',
|
||||
},
|
||||
versions: [],
|
||||
},
|
||||
]);
|
||||
const setSelectionStage = vi.fn();
|
||||
|
||||
render(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="game-mine"
|
||||
setSelectionStage={setSelectionStage}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: '发布新版本' }));
|
||||
|
||||
expect(setSelectionStage).toHaveBeenCalledWith('game-publish', {
|
||||
path: '/games/publish?game=game-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('发布页带 game 参数时进入更新模式并预填既有资料', async () => {
|
||||
authUiMock.value.user = {
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
displayName: '测试用户',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
};
|
||||
authUiMock.value.canAccessProtectedData = true;
|
||||
gameDistributionMock.listMyGames.mockResolvedValue([
|
||||
{
|
||||
id: 'game-9',
|
||||
title: '星轨防线',
|
||||
summary: '守住轨道城',
|
||||
description: '旧版资料',
|
||||
category: '动作',
|
||||
tags: ['塔防'],
|
||||
coverColor: '#d77a51',
|
||||
icon: '✦',
|
||||
author: { id: 'user-1', name: '测试用户' },
|
||||
deviceSupport: { desktop: true, mobile: false, touch: false },
|
||||
status: 'published',
|
||||
publicationRevision: 5,
|
||||
currentVersion: null,
|
||||
playCount: 1,
|
||||
createdAt: '2026-09-20T08:00:00Z',
|
||||
latestVersion: {
|
||||
versionId: 'gamever-9',
|
||||
gameId: 'game-9',
|
||||
versionNumber: 4,
|
||||
packageSha256: 'f'.repeat(64),
|
||||
packageBytes: 2048,
|
||||
status: 'published',
|
||||
publicationRevision: 5,
|
||||
reviewReason: null,
|
||||
createdAt: '2026-09-20T08:00:00Z',
|
||||
updatedAt: '2026-09-20T09:00:00Z',
|
||||
},
|
||||
versions: [],
|
||||
},
|
||||
]);
|
||||
gameDistributionMock.getGameVersion.mockResolvedValue({
|
||||
game: {
|
||||
id: 'game-9',
|
||||
title: '星轨防线',
|
||||
summary: '守住轨道城',
|
||||
description: '旧版资料',
|
||||
category: '动作',
|
||||
tags: ['塔防'],
|
||||
publicationRevision: 5,
|
||||
},
|
||||
version: {
|
||||
versionId: 'gamever-9',
|
||||
versionNumber: 4,
|
||||
status: 'published',
|
||||
publicationRevision: 5,
|
||||
recoveryAction: 'none',
|
||||
},
|
||||
});
|
||||
window.history.replaceState(null, '', '/games/publish?game=game-9');
|
||||
|
||||
render(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="game-publish"
|
||||
setSelectionStage={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByText(/正在为《星轨防线》发布新版本 v5/u),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByLabelText('游戏名称')).toHaveProperty(
|
||||
'value',
|
||||
'星轨防线',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps guide and tool intent in editor navigation URLs', async () => {
|
||||
const setSelectionStage = vi.fn();
|
||||
const { rerender } = render(
|
||||
|
||||
Reference in New Issue
Block a user