Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasGenerationTasksSidebarDismiss.test.tsx
T
k88936 25c5ecdf6e
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m58s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m34s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m29s
Project CI / Backend tests (pull_request) Failing after 4m38s
Project CI / Frontend tests (pull_request) Successful in 3m33s
Project CI / Repository checks (pull_request) Failing after 4m4s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m13s
Project CI / Native shell tests (pull_request) Successful in 6m35s
测试:全仓测试统一 Tauri 运行时替身安装点
- 78 处内联 window.__TAURI__ 赋值改为 installTauriRuntime,覆盖 appSurface 之外的 37 个测试文件
- 31 处 delete window.__TAURI__ / window.__TAURI__ = undefined 复位改为 resetTauriRuntime
- 删除 recentProjectsHook、projectCreationDirectory、projectResourceLiveIntegration、useProjectResourceCardPreviews、resourceVersionReplacementModel、unityProjectOpen 里的本地 asTauriInvoke / asTauriListen 适配器与相关类型别名
- designProjectRestore 的 setup 参数由 ReturnType<typeof vi.fn> 收窄为具体 invoke 签名
- 迁移后仅 tests/tauriRuntimeFake.ts 直接读写 window.__TAURI__
2026-10-03 20:19:45 +08:00

252 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** @vitest-environment jsdom */
import { readFileSync } from 'node:fs';
import { afterEach, describe, expect, it, vi } from 'vitest';
import ProjectDevelopmentView from '../src/view/project-development';
import {
createGameCreationAppManifest,
fireEvent,
React,
render,
screen,
waitFor,
within,
} from './appSurface/harness';
import { repoPath } from './repoPath';
import { installTauriRuntime } from './tauriRuntimeFake';
/**
* 「生成任务」侧栏与右上角入口的三条口径:
*
* 1. **失去焦点即收起**:点画布、点别的工具栏按钮、点侧栏外部都算失去焦点;点侧栏内部
* (含任务卡与「定位到素材」)与点那枚开合开关不算——后者自己负责 toggle,否则会先被
* 收起再被 toggle 打开,表现为按钮失灵。
* 2. **入口位置**:开关常驻画布**右上角**的锚点里(形态照抄美术画布),不再占工具条那一行;
* 工具条上因此只剩资源动作与排列方式。
* 3. **行内间距只有一套**:「依赖 / 类型」与前一按钮之间不能只剩分段控件自带的那点间隙。
*/
function resourceGraphFor(resources: unknown) {
const resourceIds = Array.isArray(resources)
? resources.map(
(resource) => (resource as { resourceId?: string }).resourceId ?? '',
)
: [];
return {
resourceIds,
referenceEdges: [],
taskFlows: [],
categories: [],
diagnostics: [],
};
}
function installInvoke() {
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_local_project_resource_graph') {
return resourceGraphFor(args?.resources);
}
if (command === 'read_local_project_resource_canvas_layout') {
return {
schemaVersion: 'game-creator-resource-layout.v1',
projectId: args?.expectedProjectId,
mode: args?.mode,
revision: 0,
positions: [],
updatedAt: 0,
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
// 该 mock 按命令返回固定结构,无法满足 TauriInvoke 的泛型逐命令建模,故断言为 window.__TAURI__。
installTauriRuntime({ core: { invoke } });
}
async function renderWorkbench(projectId: string) {
installInvoke();
const manifest = createGameCreationAppManifest(
projectId,
`${projectId} 项目`,
);
render(
React.createElement(ProjectDevelopmentView, {
projectName: manifest.name,
projectPath: `/tmp/${projectId}`,
manifest,
attachments: [],
recentRunStatus: null,
recentRunStopReason: null,
chat: React.createElement('div', null, '项目总控'),
onHomeOpen: vi.fn(),
onProjectsOpen: vi.fn(),
}),
);
const entry = await screen.findByRole('button', { name: '生成任务' });
fireEvent.click(entry);
return screen.findByRole('region', { name: '生成任务' });
}
afterEach(() => {
document.body.innerHTML = '';
vi.restoreAllMocks();
});
describe('「生成任务」侧栏的开合与入口位置', () => {
it('点侧栏外部(画布 / 其他区域)自动收起', async () => {
const sidebar = await renderWorkbench('workbench-tasks-dismiss-outside');
expect(sidebar).not.toBeNull();
fireEvent.pointerDown(document.body);
await waitFor(() =>
expect(screen.queryByRole('region', { name: '生成任务' })).toBeNull(),
);
});
it('展开时开关让位:点侧栏内部不收起,收起只走面板自己的 ×', async () => {
const sidebar = await renderWorkbench('workbench-tasks-dismiss-inside');
// 侧栏内部点击(任务列表区域)不该被当成点外部。
fireEvent.pointerDown(sidebar);
expect(screen.queryByRole('region', { name: '生成任务' })).not.toBeNull();
// 展开态开关不渲染(开关与面板不并排):这一刻画布上只有面板自己那枚 ×。
expect(
screen.queryByRole('button', { name: /^生成任务(?: · \d+)?$/ }),
).toBeNull();
fireEvent.click(
within(sidebar).getByRole('button', { name: '关闭生成任务' }),
);
await waitFor(() =>
expect(screen.queryByRole('region', { name: '生成任务' })).toBeNull(),
);
// 收起之后开关回来,点它能再打开。
const entry = await screen.findByRole('button', { name: '生成任务' });
fireEvent.click(entry);
expect(
await screen.findByRole('region', { name: '生成任务' }),
).not.toBeNull();
});
it('「生成任务」开关落在画布右上角锚点里,不再占工具条那一行', async () => {
await renderWorkbench('workbench-tasks-entry-order');
const actionsRow = document.querySelector('.game-workbench-view-actions');
expect(actionsRow).not.toBeNull();
const buttons = Array.from(actionsRow!.querySelectorAll('button'));
// 工具条不再有「生成任务」入口:入口只有一个,就是右上角那一枚开关。
expect(
buttons.some(
(button) => button.getAttribute('aria-label') === '生成任务',
),
).toBe(false);
const sortIndex = buttons.findIndex(
(button) => button.getAttribute('aria-label') === '按依赖',
);
expect(sortIndex).toBeGreaterThanOrEqual(0);
// 依赖 / 类型仍然收在行尾。
expect(sortIndex).toBe(buttons.length - 2);
// 锚点挂在主视窗(stage)的**画布那一格**里:右上角坐标是相对画布工作面算的,
// 不是相对整个窗口,也不会压在工具条那一行上。
const anchor = document.querySelector(
'.game-resource-generation-tasks-anchor',
);
expect(anchor).not.toBeNull();
/*
* 这一份夹具停在**资源总览**(没有打开任何栏目页),所以锚点用的是总览那一档:
* 总览页顶部没有钉死的栏目标题栏,锚点贴画布顶边内缩即可;栏目画布页会因为那条整宽的
* 标题栏(右端是「返回资源总览」)而多让开一段高度,两档的坐标与过渡见
* `resourceCanvasAssetGenerationTasksSidebarStyle.test.ts`。
*/
expect(anchor?.getAttribute('data-generation-tasks-placement')).toBe(
'canvas-overview',
);
expect(
document
.querySelector('.game-workbench-stage')
?.contains(anchor as Element),
).toBe(true);
});
it('运行页不挂「生成任务」入口与面板,切回资源页又回来', async () => {
installInvoke();
const manifest = createGameCreationAppManifest(
'workbench-run-hides-tasks-entry',
'运行页隐藏生成任务',
);
// 有已完成的可运行原型:运行页签可用,但没有活预览,所以首屏仍停在资源管理。
manifest.tasks = manifest.tasks.map((task) =>
task.id === 'code-prototype' ? { ...task, status: 'completed' } : task,
);
render(
React.createElement(ProjectDevelopmentView, {
projectName: manifest.name,
projectPath: '/tmp/workbench-run-hides-tasks-entry',
manifest,
attachments: [],
recentRunStatus: null,
recentRunStopReason: null,
chat: React.createElement('div', null, '项目总控'),
onHomeOpen: vi.fn(),
onProjectsOpen: vi.fn(),
}),
);
// 资源页照旧有入口与锚点。
expect(
await screen.findByRole('button', { name: '生成任务' }),
).not.toBeNull();
expect(
document.querySelector('.game-resource-generation-tasks-anchor'),
).not.toBeNull();
// 运行页:入口、面板、锚点一起消失,画面右上角只留版本入口与预览地址小字。
fireEvent.click(screen.getByRole('tab', { name: '运行' }));
await waitFor(() =>
expect(screen.getByLabelText('运行表现层')).not.toBeNull(),
);
expect(
document.querySelector('.game-resource-generation-tasks-anchor'),
).toBeNull();
expect(screen.queryByRole('button', { name: /^生成任务/ })).toBeNull();
// 切回资源页入口回来:任务只是在这页不显示,没有被丢掉。
fireEvent.click(screen.getByRole('tab', { name: '资源管理' }));
expect(
await screen.findByRole('button', { name: '生成任务' }),
).not.toBeNull();
});
it('「依赖 / 类型」与前一按钮之间的间距跟行内其他按钮一致', () => {
const styles = readFileSync(
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
// 这一行是 gap: 7px 的 flex 容器;分段控件曾靠自带 padding: 3px 与前一按钮只隔 10px,
// 而其余按钮之间是 24px。margin-left 把它补回 24px,整行只剩一套间距。
expect(styles).toMatch(
/\.game-workbench-tabs\.game-resource-sort-tabs\s*\{[^}]*margin-left:\s*17px/s,
);
expect(styles).toMatch(/\.game-resource-sort-tabs\s*\{[^}]*padding:\s*0/s);
});
it('布局状态提示不参与动作行排版,不会按文案宽度顶开按钮', () => {
const styles = readFileSync(
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
// 它是随保存过程变长的文案(空 →「保存中」→「布局已保存」→ 失败原因)。作为 flex 子项
// 会把右侧按钮按文本宽度顶开,同一行的按钮间距就会随时刻变化;绝对定位后位置固定。
expect(styles).toMatch(
/\.game-resource-reorder-status\s*\{[^}]*position:\s*absolute/s,
);
expect(styles).toMatch(
/\.game-resource-reorder-status\s*\{[^}]*bottom:\s*4px[^}]*left:\s*12px/s,
);
});
});