Files
Genarrative/apps/ai-game-creator-shell/tests/launcherNoticeToast.test.tsx
T
kdletters 1d6cbd4b67
Project CI / AI game creator shell Rust crates (push) Successful in 1m29s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m57s
Project CI / Backend tests (push) Successful in 3m57s
Project CI / Frontend tests (push) Successful in 2m0s
Project CI / Native shell tests (push) Successful in 6m5s
Project CI / Repository checks (push) Failing after 42s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m49s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m54s
Project CI / AI game creator shell web tests (push) Successful in 1m40s
外壳状态栏退役,状态改由浮层提示承载
- 首页输入框下方与项目组页面头部那行状态栏退役:status 不再由页面渲染,统一由外壳浮层承载(WorkspaceLauncher 合成粘性状态与一次性提示,渲染 notice ?? statusNotice)
- 进行中进度与失败结论走粘性浮层(sticky: true,不自动收起,切页回来仍可见);成功与取消回显走一次性浮层(成功/取消 2.6 秒、失败 6 秒收起),一次性提示优先显示,收起后粘性状态继续显示
- 粘性状态只挂首页与项目组两个页面,进项目工作台、模板库、帮助页不再飘在界面上;运行/预览提示与取消回显不受该限制
- RunNoticeToast/RunNotice 更名为 LauncherNoticeToast/LauncherNotice,新增 sticky 与 neutral 色调,挂点改为 data-launcher-notice-toast,长文案容器限宽 min(680px, 100vw - 2rem) 并换行
- 收起回调只从 ref 读,不再写进 effect 依赖:此前外壳每次重渲染都会换一个内联箭头函数,把收起计时重新起算,长驻页面上的提示可能永远收不回去
- useHomeProjectCreation 三处取消(非空文件夹确认弹窗、打开项目选目录、新建项目选目录)改为清空状态并弹一次性浮层回显「已取消」,不再往页面上那行写文字
- 删除 HomeView 的 status prop 与状态行节点、ProjectsPage 的 status prop 与 .launcher-project-page-status 节点及对应 CSS 规则
- launcherNoticeToast 用例补粘性不收起、父组件重渲染不重新计时、取消回显短时收起;launcherNoticeShellWiring 用例补进行中状态与取消回显都只走浮层、首页不留那一行、粘性状态不进项目工作台
- appSurface/home.suite 三处取消断言改断浮层挂点;homeWebPreflight 与 designProjectRestore 的 hook 夹具补 showTransientNotice
- 技术方案与 decision-log 记录该口径与边界:首页带按钮的「已创建的工作区」提示仍留在页面上
2026-09-29 19:35:49 +08:00

179 lines
4.7 KiB
TypeScript

/** @vitest-environment jsdom */
import { act, cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { LauncherNoticeToast } from '../src/features/app-shell/LauncherNoticeToast';
const RUN_MESSAGE = '运行通过,已载入客户端运行视图';
function toastElement() {
return document.querySelector('[data-launcher-notice-toast="true"]');
}
describe('外壳浮层提示', () => {
afterEach(() => {
cleanup();
vi.useRealTimers();
});
it('没有提示时不渲染任何浮层', () => {
render(<LauncherNoticeToast notice={null} onDismiss={vi.fn()} />);
expect(toastElement()).toBeNull();
expect(screen.queryByText(RUN_MESSAGE)).toBeNull();
});
it('在浮层里显示提示,并在到点后自动收起', () => {
vi.useFakeTimers();
const onDismiss = vi.fn();
render(
<LauncherNoticeToast
notice={{ id: 1, message: RUN_MESSAGE }}
onDismiss={onDismiss}
/>,
);
expect(toastElement()).not.toBeNull();
expect(screen.getByText(RUN_MESSAGE)).not.toBeNull();
act(() => {
vi.advanceTimersByTime(2_600);
});
expect(onDismiss).toHaveBeenCalledTimes(1);
});
it('同一句文案连续触发时重新计时', () => {
vi.useFakeTimers();
const onDismiss = vi.fn();
const { rerender } = render(
<LauncherNoticeToast
notice={{ id: 1, message: RUN_MESSAGE }}
onDismiss={onDismiss}
/>,
);
rerender(
<LauncherNoticeToast
notice={{ id: 2, message: RUN_MESSAGE }}
onDismiss={onDismiss}
/>,
);
act(() => {
vi.advanceTimersByTime(2_000);
});
expect(onDismiss).not.toHaveBeenCalled();
act(() => {
vi.advanceTimersByTime(600);
});
expect(onDismiss).toHaveBeenCalledTimes(1);
});
it('父组件重渲染(换一个内联收起回调)不重新计时', () => {
vi.useFakeTimers();
const onDismiss = vi.fn();
// 同一条提示的状态对象在外壳里是稳定的;会变的只有每次都新写的内联回调。
const notice = { id: 1, message: RUN_MESSAGE };
const { rerender } = render(
<LauncherNoticeToast notice={notice} onDismiss={onDismiss} />,
);
act(() => {
vi.advanceTimersByTime(2_000);
});
rerender(
<LauncherNoticeToast notice={notice} onDismiss={() => onDismiss()} />,
);
act(() => {
vi.advanceTimersByTime(600);
});
expect(onDismiss).toHaveBeenCalledTimes(1);
});
it('失败提示用错误色调,不需要调用方再拼一套文案', () => {
render(
<LauncherNoticeToast
notice={{
id: 1,
tone: 'error',
message: '在浏览器打开失败:permission denied',
}}
onDismiss={vi.fn()}
/>,
);
const toast = toastElement()?.querySelector(
'.platform-runtime-status-toast',
);
// `PlatformRuntimeStatusToast` 的失败态就是 alert + assertive,成功态是 status + polite。
expect(toast?.getAttribute('role')).toBe('alert');
expect(
screen.getByText('在浏览器打开失败:permission denied'),
).not.toBeNull();
});
it('失败提示比成功提示留得久:2.6 秒不该把错误收走', () => {
vi.useFakeTimers();
const onDismiss = vi.fn();
render(
<LauncherNoticeToast
notice={{
id: 1,
tone: 'error',
message: '运行游戏失败:预览端口被占用',
}}
onDismiss={onDismiss}
/>,
);
act(() => {
vi.advanceTimersByTime(2_600);
});
expect(onDismiss).not.toHaveBeenCalled();
act(() => {
vi.advanceTimersByTime(3_400);
});
expect(onDismiss).toHaveBeenCalledTimes(1);
});
it('取消回显走同样的浮层,短时收起', () => {
vi.useFakeTimers();
const onDismiss = vi.fn();
render(
<LauncherNoticeToast
notice={{ id: 1, tone: 'neutral', message: '已取消' }}
onDismiss={onDismiss}
/>,
);
expect(screen.getByText('已取消')).not.toBeNull();
expect(toastElement()?.querySelector('[role="status"]')).not.toBeNull();
act(() => {
vi.advanceTimersByTime(2_600);
});
expect(onDismiss).toHaveBeenCalledTimes(1);
});
it('粘性提示(承接原状态栏)不自动收起', () => {
vi.useFakeTimers();
const onDismiss = vi.fn();
render(
<LauncherNoticeToast
notice={{ id: 1, sticky: true, message: '正在创建工作区' }}
onDismiss={onDismiss}
/>,
);
act(() => {
vi.advanceTimersByTime(60_000);
});
expect(onDismiss).not.toHaveBeenCalled();
expect(screen.getByText('正在创建工作区')).not.toBeNull();
});
});