8701f43f75
- 删除渲染层传输层:services/clientApi.ts、clientHttp 的 fetchClientHttp 与超时封装整体退场,账户、认证、素材直传、错误上报与游戏分发发布改走 Rust typed command。 - 新增宿主封装:accountHost、editorAssetHost、platformSessionRetry、agentRuntimeErrorDetail 与 features/app-shell/workspacePreferences;渲染层不再读写 access token / refresh cookie,localStorage 只剩测试态旧键迁移 shim。 - 修复 DirectProject 失败文案 v2 映射与「查看详情」按需读取,画布标签面板高度夹紧、框选每帧重算、文档预览入口与场景标题栏 key、资源筛选面板样式。 - 补齐与调整回归:accountHost、agentRuntimeErrorDetail、clientAuthHost、clientHttp、platformSession、platformSessionRetry、resourceFilterPanelStyle、directThreadChatSubscription 等定向用例,并清理测试 harness 中无调用方的残留。
307 lines
11 KiB
TypeScript
307 lines
11 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act, cleanup, renderHook, waitFor } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
|
import { useHomeProjectCreation } from '../src/features/app-shell/useHomeProjectCreation';
|
|
import type {
|
|
GameTemplateEntry,
|
|
GameTemplateLibrarySnapshot,
|
|
} from '../src/features/template-library/templateLibraryModel';
|
|
import { useTemplateLibrary } from '../src/features/template-library/useTemplateLibrary';
|
|
import {
|
|
beginPlatformSessionClearTransition,
|
|
resetPlatformSessionForTests as resetPlatformSessionStateForTests,
|
|
} from '../src/services/platformSession';
|
|
|
|
const invoke = vi.hoisted(() => vi.fn());
|
|
vi.mock('../src/app/tauri', () => ({ resolveTauriInvoke: () => invoke }));
|
|
|
|
const template: GameTemplateEntry = {
|
|
id: 'demo',
|
|
title: '演示',
|
|
summary: '',
|
|
tags: [],
|
|
runtime: 'html',
|
|
engine: 'phaser',
|
|
engineVersion: '4',
|
|
templateVersion: '1',
|
|
updatedAt: '',
|
|
entry: 'index.html',
|
|
zipUrl: 'https://example.invalid/demo.zip',
|
|
zipSizeBytes: 1,
|
|
zipSha256: 'a'.repeat(64),
|
|
coverUrl: '',
|
|
coverWidth: 100,
|
|
coverHeight: 100,
|
|
installed: true,
|
|
installedVersion: '1',
|
|
installedAtMillis: 1,
|
|
};
|
|
const snapshot = (id = 'demo'): GameTemplateLibrarySnapshot => ({
|
|
schemaVersion: 'agc-template-library.v1',
|
|
library: 'official',
|
|
libraryVersion: 1,
|
|
updatedAt: '',
|
|
fetchedAtMillis: 1,
|
|
source: 'cache',
|
|
templates: [{ ...template, id }],
|
|
});
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void;
|
|
const promise = new Promise<T>((done) => {
|
|
resolve = done;
|
|
});
|
|
return { promise, resolve };
|
|
}
|
|
const onProjectCreated = vi.fn();
|
|
const mount = () =>
|
|
renderHook(({ userId }) => useTemplateLibrary({ userId, onProjectCreated }), {
|
|
initialProps: { userId: 'user-a' },
|
|
});
|
|
|
|
beforeEach(() => {
|
|
resetPlatformSessionStateForTests();
|
|
invoke.mockReset();
|
|
onProjectCreated.mockReset();
|
|
});
|
|
afterEach(cleanup);
|
|
|
|
describe('模板库灰度会话', () => {
|
|
it('等待权威权限时不读取清单,拒绝后也不能调用下载', async () => {
|
|
const access = deferred<boolean>();
|
|
invoke.mockImplementation(() => access.promise);
|
|
const { result } = mount();
|
|
expect(result.current.enabled).toBe(false);
|
|
expect(invoke.mock.calls.map(([command]) => command)).toEqual([
|
|
'get_game_template_library_access',
|
|
]);
|
|
await act(async () => {
|
|
access.resolve(false);
|
|
});
|
|
await expect(result.current.downloadTemplate(template)).rejects.toThrow(
|
|
'暂未',
|
|
);
|
|
expect(result.current.snapshot).toBeNull();
|
|
expect(invoke).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('命中后读取清单,窗口重新聚焦失去权限就清空缓存投影', async () => {
|
|
let allowed = true;
|
|
invoke.mockImplementation(async (command) =>
|
|
command === 'get_game_template_library_access' ? allowed : snapshot(),
|
|
);
|
|
const { result } = mount();
|
|
await waitFor(() => expect(result.current.status).toBe('ready'));
|
|
expect(result.current.enabled).toBe(true);
|
|
expect(result.current.templates).toHaveLength(1);
|
|
allowed = false;
|
|
await act(async () => {
|
|
window.dispatchEvent(new Event('focus'));
|
|
});
|
|
expect(result.current.enabled).toBe(false);
|
|
expect(result.current.templates).toEqual([]);
|
|
expect(result.current.snapshot).toBeNull();
|
|
expect(
|
|
invoke.mock.calls.filter(
|
|
([command]) => command === 'fetch_game_template_library',
|
|
),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
it('账号切换后丢弃前一账号迟到的允许结果', async () => {
|
|
const access = deferred<boolean>();
|
|
invoke
|
|
.mockImplementationOnce(() => access.promise)
|
|
.mockResolvedValue(false);
|
|
const { result, rerender } = mount();
|
|
rerender({ userId: 'user-b' });
|
|
await act(async () => {
|
|
access.resolve(true);
|
|
});
|
|
expect(result.current.enabled).toBe(false);
|
|
expect(
|
|
invoke.mock.calls.filter(
|
|
([command]) => command === 'fetch_game_template_library',
|
|
),
|
|
).toHaveLength(0);
|
|
});
|
|
|
|
it('账号切换后旧清单不能覆盖当前账号的清单', async () => {
|
|
const oldManifest = deferred<GameTemplateLibrarySnapshot>();
|
|
let fetches = 0;
|
|
invoke.mockImplementation(async (command) => {
|
|
if (command === 'get_game_template_library_access') return true;
|
|
return ++fetches === 1
|
|
? oldManifest.promise
|
|
: snapshot('user-b-template');
|
|
});
|
|
const { result, rerender } = mount();
|
|
await waitFor(() => expect(fetches).toBe(1));
|
|
rerender({ userId: 'user-b' });
|
|
await waitFor(() =>
|
|
expect(result.current.templates[0]?.id).toBe('user-b-template'),
|
|
);
|
|
await act(async () => {
|
|
oldManifest.resolve(snapshot('user-a-template'));
|
|
});
|
|
expect(result.current.templates[0]?.id).toBe('user-b-template');
|
|
});
|
|
|
|
it.each(['download', 'create'] as const)(
|
|
'账号切换后迟到的%s结果不能写状态或跳转',
|
|
async (operation) => {
|
|
const completion = deferred<unknown>();
|
|
let allowed = true;
|
|
invoke.mockImplementation(async (command) => {
|
|
if (command === 'get_game_template_library_access') return allowed;
|
|
if (command === 'fetch_game_template_library') return snapshot();
|
|
return completion.promise;
|
|
});
|
|
const { result, rerender } = mount();
|
|
await waitFor(() => expect(result.current.status).toBe('ready'));
|
|
let pending!: Promise<unknown>;
|
|
act(() => {
|
|
pending = (
|
|
operation === 'create'
|
|
? result.current.createProjectFromTemplate(template)
|
|
: result.current.downloadTemplate(template)
|
|
).catch((error) => error);
|
|
});
|
|
allowed = false;
|
|
rerender({ userId: 'user-b' });
|
|
await act(async () => {
|
|
completion.resolve({ projectPath: '/old', templateVersion: '1' });
|
|
await pending;
|
|
});
|
|
expect(await pending).toBeInstanceOf(Error);
|
|
expect(onProjectCreated).not.toHaveBeenCalled();
|
|
expect(result.current.enabled).toBe(false);
|
|
expect(result.current.notice).toBe('');
|
|
expect(result.current.busyTemplateId).toBeNull();
|
|
},
|
|
);
|
|
|
|
it('原生命令拒绝权限后立即关闭入口,已安装模板不能绕过', async () => {
|
|
invoke.mockImplementation(async (command) => {
|
|
if (command === 'get_game_template_library_access') return true;
|
|
if (command === 'fetch_game_template_library') return snapshot();
|
|
throw new Error('template-library-unavailable: 模板库暂未开放');
|
|
});
|
|
const { result } = mount();
|
|
await waitFor(() => expect(result.current.status).toBe('ready'));
|
|
await act(async () => {
|
|
await expect(
|
|
result.current.createProjectFromTemplate(template),
|
|
).rejects.toThrow('暂未开放');
|
|
});
|
|
expect(result.current.enabled).toBe(false);
|
|
expect(result.current.snapshot).toBeNull();
|
|
expect(onProjectCreated).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('检查权限失败时关闭入口而不使用先前允许结果', async () => {
|
|
invoke.mockImplementation(async (command) =>
|
|
command === 'get_game_template_library_access' ? true : snapshot(),
|
|
);
|
|
const { result } = mount();
|
|
await waitFor(() => expect(result.current.status).toBe('ready'));
|
|
invoke.mockRejectedValueOnce(new Error('offline'));
|
|
await act(async () => {
|
|
await result.current.refresh();
|
|
});
|
|
expect(result.current.enabled).toBe(false);
|
|
expect(result.current.templates).toEqual([]);
|
|
});
|
|
|
|
it('退出登录开始时即使 userId 未变也清空权限并停止旧建项回调', async () => {
|
|
const completion = deferred<unknown>();
|
|
invoke.mockImplementation(async (command) => {
|
|
if (command === 'get_game_template_library_access') return true;
|
|
if (command === 'fetch_game_template_library') return snapshot();
|
|
return completion.promise;
|
|
});
|
|
const { result } = mount();
|
|
await waitFor(() => expect(result.current.status).toBe('ready'));
|
|
let pending!: Promise<unknown>;
|
|
act(() => {
|
|
pending = result.current
|
|
.createProjectFromTemplate(template)
|
|
.catch((error) => error);
|
|
});
|
|
act(() => {
|
|
beginPlatformSessionClearTransition();
|
|
});
|
|
expect(result.current.enabled).toBe(false);
|
|
const count = invoke.mock.calls.length;
|
|
await act(async () => {
|
|
await result.current.refresh();
|
|
});
|
|
expect(invoke).toHaveBeenCalledTimes(count);
|
|
await act(async () => {
|
|
completion.resolve({ projectPath: '/old' });
|
|
await pending;
|
|
});
|
|
expect(onProjectCreated).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each(['get_local_game_project_revision', 'get_local_game_preview_status'])(
|
|
'退出登录时 %s 的旧结果不能进入项目或登记最近项目',
|
|
async (delayedCommand) => {
|
|
const delayed = deferred<unknown>();
|
|
const manifest = createGameCreationAppManifest(
|
|
'template-project',
|
|
'模板项目',
|
|
);
|
|
invoke.mockImplementation(async (command) => {
|
|
if (command === 'get_game_template_library_access') return true;
|
|
if (command === 'fetch_game_template_library') return snapshot();
|
|
if (command === 'create_automatic_local_game_project_from_template')
|
|
return { projectPath: 'C:/test/template-project', manifest };
|
|
if (command === delayedCommand) return delayed.promise;
|
|
if (command === 'get_local_game_project_revision')
|
|
return { revision: 1 };
|
|
if (command === 'get_local_game_preview_status')
|
|
return { status: 'stopped' };
|
|
throw new Error(command);
|
|
});
|
|
const setLauncherView = vi.fn();
|
|
const rememberRecentWorkspace = vi.fn();
|
|
const { result } = renderHook(() => {
|
|
const home = useHomeProjectCreation({
|
|
setLauncherView,
|
|
rememberRecentWorkspace,
|
|
setStatus: vi.fn(),
|
|
setAgentChatProjectPath: vi.fn(),
|
|
});
|
|
const library = useTemplateLibrary({
|
|
userId: 'user-a',
|
|
onProjectCreated: (created, isCurrent) =>
|
|
home.enterCreatedTemplateProject(created, isCurrent),
|
|
});
|
|
return { home, library };
|
|
});
|
|
await waitFor(() => expect(result.current.library.status).toBe('ready'));
|
|
let pending!: Promise<unknown>;
|
|
act(() => {
|
|
pending = result.current.library.createProjectFromTemplate(template);
|
|
});
|
|
await waitFor(() =>
|
|
expect(
|
|
invoke.mock.calls.some(([command]) => command === delayedCommand),
|
|
).toBe(true),
|
|
);
|
|
act(() => {
|
|
beginPlatformSessionClearTransition();
|
|
});
|
|
await act(async () => {
|
|
delayed.resolve({ revision: 1, status: 'stopped' });
|
|
await pending;
|
|
});
|
|
expect(result.current.home.currentProjectContext).toBeNull();
|
|
expect(setLauncherView).not.toHaveBeenCalled();
|
|
expect(rememberRecentWorkspace).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
});
|