beebcca4de
- 生成任务锚点新增「资源总览」档:资源总览页仍贴画布顶边内缩 0.85rem,资源栏目画布页让开顶部钉住的整宽栏目标题栏(42px + 0.5rem),两档切换走 margin-top 过渡(减动效下关掉)。 - 生成浮层的顶边与可用高度改为按占位卡的真实屏幕位置算(卡下沿到安全底边),不再假设卡片贴在安全带上沿——占位落到下半屏时浮层底边不再越过画布下沿被切掉。 - 生成浮层新增水平收口:按「浮层左右各留 12px」把锚点收进画布可视范围,卡片靠边时浮层不再有一半落到画布之外;宽度口径与面板 CSS 同源(min(560px, 100% - 24px))。 - 新增画布卡片手势的共享判据 resourceCanvasGestureExceededDragThreshold(阈值仍取 RESOURCE_CANVAS_DRAG_THRESHOLD):资源卡与生成占位卡共用同一份「拖动还是点击」判断。 - 生成占位卡修掉拖动收尾误触点击:越过阈值才算拖动、登记一次性点击抑制(consumeDragClick)由宿主消费,「拖动中」样式也从越过阈值那一刻起才亮。 - 生成占位卡的删除按钮起手手势不再被当成「点卡片」:按起手点判定,松手落回卡片时不触发开合生成浮层。 - 图标规范面板的参考选择注释按现行判据改写(只有图集 icon-spritesheet 不接受用户参考),入口矩阵文档同步补「参考图(@ 引用)」口径并修掉「本地 IPC 没有参考图入参」的旧说法。 - 补齐回归用例:手势模型判据、占位卡拖动/点击与阈值、宿主级「拖完不弹浮层且普通点击仍开合」、浮层顶边与水平收口、锚点分档与跨文件标题栏高度守卫、逐条生成入口的 @ 与判据一致、图标规范面板的 @ 与提交带引用。 - 共享记忆 pitfalls.md 记录两条排障口径:卡类手势必须只有一份阈值与点击抑制判据;画布浮层几何只能按真实屏幕位置算、分页锚点必须让开钉死的标题栏。
162 lines
6.3 KiB
TypeScript
162 lines
6.3 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 } 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();
|
|
const onSubmit = vi.fn(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));`,
|
|
);
|
|
});
|
|
});
|