45ae29461c
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m7s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m38s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m42s
Project CI / Frontend tests (pull_request) Successful in 3m37s
Project CI / Backend tests (pull_request) Successful in 8m12s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m54s
Project CI / Repository checks (pull_request) Successful in 6m58s
Project CI / Native shell tests (pull_request) Successful in 9m32s
- 面板形态:快照读取成功但没有在跑回合时不再渲染「未能读取正在运行的项目」,补回 !readFailed 守卫 - 发布门控:currentProjectPath / currentProjectName 只在 launcherView === 'project-development' 时发布,离开工作台按口径回落到最近启动的在跑项目 - 菜单头部只报运行中数量,读取失败结论由 role=status 一行独占,避免同一句连排两遍 - 标签提示拆成「作用域 + 运行状态」:回落态记为「最近运行的项目:X」、状态位「共 N 个项目在运行」,不再重复「正在运行」 - 数量徽标补 title 说明数字含义 - 用例:新增面板空态回归与两条视图门控回归,更新两处标签断言;三条定向文件 25 passed - 文档:决策日志补 2026-10-05 评审后续条目并修正实施计划交叉引用,里程碑补 2026-10-05 修订,实施计划与主规范同步口径
244 lines
7.7 KiB
TypeScript
244 lines
7.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { APP_NAME } from '../src/app/appMetadata';
|
|
import type { GameCreatorDirectActiveTurn } from '../src/app/types';
|
|
import { ThemedModal } from '../src/components/modal/ThemedModal';
|
|
import { WindowChrome } from '../src/components/WindowChrome';
|
|
import { useWindowChrome } from '../src/components/windowChromeContext';
|
|
|
|
const nativeWindow = vi.hoisted(() => ({
|
|
minimize: vi.fn(),
|
|
toggleMaximize: vi.fn(),
|
|
isMaximized: vi.fn(),
|
|
close: vi.fn(),
|
|
label: 'client',
|
|
}));
|
|
|
|
vi.mock('@tauri-apps/api/window', () => ({
|
|
getCurrentWindow: () => nativeWindow,
|
|
}));
|
|
|
|
function TitleSetter({ value }: { value: string }) {
|
|
const { setTitle } = useWindowChrome();
|
|
return (
|
|
<button type="button" onClick={() => setTitle(value)}>
|
|
更新标题
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function ActiveRunsSetter({
|
|
activeTurns,
|
|
currentProjectName,
|
|
currentProjectPath,
|
|
}: {
|
|
activeTurns: GameCreatorDirectActiveTurn[];
|
|
currentProjectName?: string;
|
|
currentProjectPath?: string;
|
|
}) {
|
|
const { setActiveProjectRuns } = useWindowChrome();
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
setActiveProjectRuns({
|
|
activeTurns,
|
|
...(currentProjectName ? { currentProjectName } : {}),
|
|
...(currentProjectPath ? { currentProjectPath } : {}),
|
|
onOpenProject: () => undefined,
|
|
})
|
|
}
|
|
>
|
|
显示运行项目
|
|
</button>
|
|
);
|
|
}
|
|
|
|
describe('WindowChrome', () => {
|
|
beforeEach(() => {
|
|
nativeWindow.minimize.mockReset();
|
|
nativeWindow.toggleMaximize.mockReset();
|
|
nativeWindow.isMaximized.mockReset();
|
|
nativeWindow.close.mockReset();
|
|
delete (window as unknown as Record<string, unknown>).__TAURI_INTERNALS__;
|
|
});
|
|
|
|
it('renders the陶泥儿 brand, default title, and controls', async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<WindowChrome>
|
|
<div>工作区内容</div>
|
|
</WindowChrome>,
|
|
);
|
|
|
|
expect(screen.getByRole('banner', { name: '窗口标题栏' })).toBeTruthy();
|
|
expect(screen.getByLabelText(`${APP_NAME} GameAgent`)).toBeTruthy();
|
|
expect(screen.queryByLabelText('本地工作区')).toBeNull();
|
|
expect(screen.getByText('创作工作台')).toBeTruthy();
|
|
expect(screen.getByText('工作区内容')).toBeTruthy();
|
|
expect(screen.queryByRole('menubar')).toBeNull();
|
|
|
|
await user.click(screen.getByRole('button', { name: '最小化' }));
|
|
await user.click(screen.getByRole('button', { name: '最大化' }));
|
|
await user.click(screen.getByRole('button', { name: '关闭' }));
|
|
});
|
|
|
|
it('updates the centered title through the shell context', async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<WindowChrome>
|
|
<TitleSetter value="星尘花园" />
|
|
</WindowChrome>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', { name: '更新标题' }));
|
|
|
|
expect(screen.getByText('星尘花园')).toBeTruthy();
|
|
expect(screen.getByLabelText('当前工作区:星尘花园')).toBeTruthy();
|
|
});
|
|
|
|
it('restores the default title when a page clears its title', async () => {
|
|
const user = userEvent.setup();
|
|
function ClearTitle() {
|
|
const { setTitle } = useWindowChrome();
|
|
return (
|
|
<button type="button" onClick={() => setTitle(' ')}>
|
|
清空标题
|
|
</button>
|
|
);
|
|
}
|
|
render(
|
|
<WindowChrome>
|
|
<ClearTitle />
|
|
</WindowChrome>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', { name: '清空标题' }));
|
|
|
|
expect(screen.getByText('创作工作台')).toBeTruthy();
|
|
});
|
|
|
|
it('closes no menus because the title bar has no application menu bar', () => {
|
|
render(
|
|
<WindowChrome>
|
|
<button type="button">页面按钮</button>
|
|
</WindowChrome>,
|
|
);
|
|
|
|
fireEvent.pointerDown(screen.getByRole('button', { name: '页面按钮' }));
|
|
expect(screen.queryByRole('menu')).toBeNull();
|
|
});
|
|
|
|
it('renders the latest active project in the title bar and expands the full list', async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<WindowChrome>
|
|
<ActiveRunsSetter
|
|
activeTurns={[
|
|
{
|
|
projectPath: 'C:/projects/first',
|
|
projectName: '先开始',
|
|
turnId: 'turn-first',
|
|
startedAt: 100,
|
|
status: 'running',
|
|
updatedAt: 120,
|
|
sequence: 1,
|
|
},
|
|
{
|
|
projectPath: 'C:/projects/later',
|
|
projectName: '后开始',
|
|
turnId: 'turn-later',
|
|
startedAt: 200,
|
|
status: 'streaming',
|
|
updatedAt: 220,
|
|
sequence: 2,
|
|
},
|
|
]}
|
|
/>
|
|
</WindowChrome>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', { name: '显示运行项目' }));
|
|
expect(
|
|
screen.getByRole('button', { name: /最近运行的项目:后开始/ }),
|
|
).toBeTruthy();
|
|
expect(screen.queryByRole('menu')).toBeNull();
|
|
await user.click(
|
|
screen.getByRole('button', { name: /最近运行的项目:后开始/ }),
|
|
);
|
|
expect(screen.getAllByRole('menuitem')).toHaveLength(2);
|
|
});
|
|
|
|
/**
|
|
* 回归:进项目后当前项目没在跑、后台别的项目在跑时,标题栏曾经整块换成
|
|
* 「正在运行的项目」,当前项目名在窗口上消失。
|
|
*/
|
|
it('keeps the current project in the title bar while another project runs', async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<WindowChrome>
|
|
<ActiveRunsSetter
|
|
activeTurns={[
|
|
{
|
|
projectPath: 'C:/projects/other',
|
|
projectName: '别的项目',
|
|
turnId: 'turn-other',
|
|
startedAt: 100,
|
|
status: 'running',
|
|
updatedAt: 120,
|
|
sequence: 1,
|
|
},
|
|
]}
|
|
currentProjectName="飞鸟 v2"
|
|
currentProjectPath="C:/projects/bird"
|
|
/>
|
|
</WindowChrome>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', { name: '显示运行项目' }));
|
|
|
|
expect(screen.getByText('飞鸟 v2')).toBeTruthy();
|
|
expect(screen.queryByText('别的项目')).toBeNull();
|
|
expect(
|
|
screen.getByRole('button', {
|
|
name: '当前项目:飞鸟 v2;共 1 个项目在运行',
|
|
}),
|
|
).toBeTruthy();
|
|
});
|
|
|
|
/**
|
|
* 回归:发布面板等 ThemedModal 弹窗打开时,标题栏在模态之外,焦点陷阱曾把
|
|
* 标题栏上的点击一起拦下 —— 三个窗口按钮看着正常但点不动。
|
|
*/
|
|
it('keeps the window controls working while a modal covers the workspace', async () => {
|
|
const user = userEvent.setup();
|
|
nativeWindow.minimize.mockResolvedValue(undefined);
|
|
nativeWindow.toggleMaximize.mockResolvedValue(undefined);
|
|
nativeWindow.close.mockResolvedValue(undefined);
|
|
nativeWindow.isMaximized.mockResolvedValue(false);
|
|
(window as unknown as Record<string, unknown>).__TAURI_INTERNALS__ = {};
|
|
|
|
render(
|
|
<WindowChrome>
|
|
<ThemedModal open onClose={() => undefined} ariaLabel="测试弹窗">
|
|
<button type="button">确认</button>
|
|
</ThemedModal>
|
|
</WindowChrome>,
|
|
);
|
|
await screen.findByRole('dialog', { name: '测试弹窗' });
|
|
|
|
await user.click(screen.getByRole('button', { name: '最小化' }));
|
|
expect(nativeWindow.minimize).toHaveBeenCalledTimes(1);
|
|
|
|
await user.click(screen.getByRole('button', { name: '最大化' }));
|
|
expect(nativeWindow.toggleMaximize).toHaveBeenCalledTimes(1);
|
|
|
|
await user.click(screen.getByRole('button', { name: '关闭' }));
|
|
expect(nativeWindow.close).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|