Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasGenerationFloatingPanelChrome.test.tsx
T
k88936 594063a1e0 测试:修正 AGC 测试文件的类型错误
- 按 vitest 0.34 的参数元组口径改写 mock 泛型
- 修正 noUncheckedIndexedAccess 下的下标访问
- 补齐与当前类型脱节的测试桩字段
- 按 scripts/*.mjs 的运行时契约补局部类型声明
2026-10-03 17:41:30 +08:00

168 lines
6.5 KiB
TypeScript

// @vitest-environment jsdom
import { readFileSync } from 'node:fs';
import { cleanup, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, test, vi } from 'vitest';
import { ResourceCanvasAssetGenerationPanelView } from '../src/features/resource-canvas/ResourceCanvasAssetGenerationPanelView';
import type { ResourceCanvasAssetToolAction } from '../src/features/resource-canvas/resourceCanvasBottomToolbarModel';
import {
ResourceCanvasGenerationPanelView,
type ResourceCanvasGenerationSubmitInput,
} from '../src/features/resource-canvas/ResourceCanvasGenerationPanelView';
import {
RESOURCE_CANVAS_GENERATION_PANEL_MAX_WIDTH,
RESOURCE_CANVAS_GENERATION_PANEL_VIEWPORT_INSET,
} from '../src/features/resource-canvas/resourceCanvasGenerationVisibilityModel';
import { repoPath } from './repoPath';
afterEach(cleanup);
const imageAction: ResourceCanvasAssetToolAction = {
id: 'generate-image',
route: 'asset',
label: '生成图片',
assetKind: 'image',
audioKind: null,
assetName: 'AI 生成图片',
promptPlaceholder: '今天想生成什么画面?',
adjustableDimensions: true,
aspectRatio: '1:1',
imageSize: '1K',
requiresIconSpecReference: false,
writesIconSpecReference: false,
};
describe('生成浮层的面板外观与高度合同', () => {
test('浮层复用模态同一套面板 chrome 类,不另造一套外观', () => {
render(
<ResourceCanvasAssetGenerationPanelView
action={imageAction}
variant="floating"
style={{ left: 120, top: 346, maxHeight: '348px' }}
assets={[]}
projectPath="/tmp/project"
onSubmit={() => undefined}
onClose={() => undefined}
/>,
);
const panel = screen.getByRole('dialog', { name: '生成图片' });
// 少了 `game-approval-dialog`,浮层就会变成没有背景/边框、header 挤在一起的一块裸容器。
expect(panel.classList.contains('game-approval-dialog')).toBe(true);
expect(panel.classList.contains('game-resource-generation-dialog')).toBe(
true,
);
expect(
panel.classList.contains('resource-canvas-generation-floating-panel'),
).toBe(true);
// 位置与高度上界都由宿主按真实矩形给:内联样式必须原样落到面板上。
expect(panel.style.top).toBe('346px');
expect(panel.style.left).toBe('120px');
expect(panel.style.maxHeight).toBe('348px');
});
test('音频入口的浮层同样带 chrome 类,并复用宿主给的提交身份重试', async () => {
const user = userEvent.setup();
// 显式泛型:否则 mock.calls 被推成空元组,下面取提交入参会报越界。
const onSubmit = vi.fn<[input: ResourceCanvasGenerationSubmitInput], void>(
async () => undefined,
);
render(
<ResourceCanvasGenerationPanelView
kinds={['background-music']}
initialKind="background-music"
variant="floating"
style={{ left: 40, top: 200, maxHeight: '300px' }}
request={{
operationId: 'operation-bound',
idempotencyKey: 'key-bound',
prompt: '轻快的八音盒',
}}
initialDraft={{
kind: 'background-music',
prompt: '轻快的八音盒',
assetName: '新背景音乐',
}}
onSubmit={onSubmit}
onClose={() => undefined}
/>,
);
const panel = screen.getByRole('dialog', { name: '生成背景音乐' });
expect(panel.classList.contains('game-approval-dialog')).toBe(true);
expect(
panel.classList.contains('resource-canvas-generation-floating-panel'),
).toBe(true);
// 收起时保存的草稿要灌回来:不是空表单。
expect(
within(panel).getByLabelText('生成提示词').getAttribute('value'),
).toBeNull();
await user.click(
within(panel).getByRole('button', { name: '生成背景音乐' }),
);
// 幂等重试:命中宿主记下的同一对 operationId / 幂等键,不铸造新的付费生成。
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit.mock.calls[0]?.[0]).toMatchObject({
kind: 'background-music',
operationId: 'operation-bound',
idempotencyKey: 'key-bound',
prompt: '轻快的八音盒',
});
});
});
/**
* 结构 CSS 钉子:真实浏览器 1280x720 上浮层曾经「面板垂到底栏下面、提交按钮点不到」,
* 而且第一版浮层只有 `game-resource-generation-dialog` 一个类,连背景边框都没有。
* 这些是几何模型测不到的部分,所以直接钉住样式文件里的关键声明。
*/
describe('生成浮层样式结构', () => {
const panelCss = () =>
readFileSync(
repoPath(
'apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasGenerationPanel.css',
),
'utf8',
);
test('浮层选择器带上面板 chrome 类,并限制自身滚动与兜底上界', () => {
const css = panelCss();
expect(css).toContain(
'.game-approval-dialog.resource-canvas-generation-floating-panel {',
);
// 锚点给的是占位卡中心:少了这行浮层会整体右偏半个面板宽(浏览器实测 x287 vs 卡中心 286)。
expect(css).toContain('transform: translateX(-50%);');
expect(css).toContain('overflow-y: auto;');
expect(css).toContain('overscroll-behavior: contain;');
// 兜底上界必须在:内联 maxHeight 拿不到几何时也不能整块垂出画布。
expect(css).toMatch(
/\.game-approval-dialog\.resource-canvas-generation-floating-panel \{[\s\S]*?max-height:/u,
);
});
test('动作行固定在面板底部,滚动内容不会把提交按钮顶出可视区', () => {
const css = panelCss();
expect(css).toMatch(
/\.game-approval-dialog\.resource-canvas-generation-floating-panel[\s\S]*?\.game-resource-generation-actions \{[\s\S]*?position: sticky;/u,
);
expect(css).toMatch(
/\.game-resource-generation-actions \{[\s\S]*?bottom: 0;[\s\S]*?background:/u,
);
});
/**
* 宿主算「水平收口」用的是模型里那对常量(浮层最大宽 + 左右内缩),CSS 这里是同一对数的另一份
* 写法:两边必须一致,否则收口算的是 560 宽、画出来的却是另一个宽度,卡片靠边时又会露出去。
*/
test('宽度口径与宿主算水平收口的常量同源', () => {
expect(panelCss()).toContain(
`width: min(${RESOURCE_CANVAS_GENERATION_PANEL_MAX_WIDTH}px, calc(100% - ${
RESOURCE_CANVAS_GENERATION_PANEL_VIEWPORT_INSET * 2
}px));`,
);
});
});