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 / 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 版本文档
250 lines
9.3 KiB
TypeScript
250 lines
9.3 KiB
TypeScript
/** @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,
|
||
);
|
||
});
|
||
});
|