Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasGenerationTasksSidebarDismiss.test.tsx
suzmii 0a8debef56
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
合并 master:运行页入口收口并入 DirectProject 重构
- 保留 master 的 DirectProject / ProjectChat 重构与测试重组:原 appSurface 的 project-commands / project-preview / supervisor-runtime 三个 suite 已被 master 删除,接受删除并把相关断言重写到 tests/previewActivation.test.tsx 等新用例
- 本分支改动重放到 master 结构:运行与预览成功的反馈改走 onRunNotice toast(带 tone),对话区不再写过程提示
- 运行页顶栏统一:预览地址改成「在浏览器打开」按钮,版本入口搬进同一动作区复用同一套按钮皮,状态行与预览地址小字退役
- 生成任务入口、面板与锚点不在运行页渲染,placement 里的 run 档一并删除
- 手工合并 App.tsx / WorkspaceLauncher.tsx / model.ts / check-config.mjs / decision-log / pitfalls,并把两条决策记录与一条排障记录补进 master 版本文档
2026-09-22 12:50:19 +08:00

250 lines
9.3 KiB
TypeScript
Raw Permalink 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';
/**
* 「生成任务」侧栏与右上角入口的三条口径:
*
* 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}`);
},
);
window.__TAURI__ = { 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,
supervisor: 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,
);
});
});