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

272 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from 'vitest';
import {
clampResourceCanvasGenerationPanelAnchorX,
resolveResourceCanvasGenerationPanelPlacement,
RESOURCE_CANVAS_GENERATION_PANEL_MAX_WIDTH,
RESOURCE_CANVAS_GENERATION_PANEL_VIEWPORT_INSET,
resourceCanvasGenerationPanelWidth,
revealResourceCanvasGenerationContent,
} from '../src/features/resource-canvas/resourceCanvasGenerationVisibilityModel';
const canvasSize = { width: 800, height: 600 };
const insets = { top: 60, bottom: 90 };
describe('生成浮层与占位的可见性', () => {
test('内容已经在安全区内时坐标不变(宿主据此跳过写回)', () => {
const viewport = { x: 0, y: 0, scale: 1 };
const next = revealResourceCanvasGenerationContent({
viewport,
content: { x: 40, y: 80, width: 180, height: 200 },
canvasSize,
insets,
});
expect(next).toEqual(viewport);
});
test('内容落到视口下方时做最小上移,底边贴住底栏安全区', () => {
const viewport = { x: 0, y: 0, scale: 1 };
// 占位在 y=550、加上浮层后整块落到视口外——真实浏览器上复现过的那一档。
const next = revealResourceCanvasGenerationContent({
viewport,
content: { x: 40, y: 550, width: 180, height: 320 },
canvasSize,
insets,
});
const safeBottom = canvasSize.height - insets.bottom;
expect(viewport.y + (550 + 320)).toBeGreaterThan(safeBottom);
expect(next.y + 550 + 320).toBe(safeBottom);
expect(next.x).toBe(0);
expect(next.scale).toBe(1);
});
test('内容顶出顶栏时下移,内容越出左右边时做最小横移', () => {
const next = revealResourceCanvasGenerationContent({
viewport: { x: 0, y: 0, scale: 1 },
content: { x: -300, y: -40, width: 180, height: 120 },
canvasSize,
insets,
});
expect(next.y).toBe(insets.top + 40);
expect(next.x).toBe(12 + 300);
});
test('缩放参与换算:缩小后的越界按同一口径平移,比例保持不变', () => {
const next = revealResourceCanvasGenerationContent({
viewport: { x: 0, y: 0, scale: 0.5 },
content: { x: 40, y: 900, width: 180, height: 320 },
canvasSize,
insets,
});
expect(next.scale).toBe(0.5);
const safeBottom = canvasSize.height - insets.bottom;
expect(next.y + (900 + 320) * 0.5).toBe(safeBottom);
});
test('内容比安全区还大时以顶边 / 左边对齐,不来回抖', () => {
const next = revealResourceCanvasGenerationContent({
viewport: { x: 0, y: 0, scale: 1 },
content: { x: 0, y: 0, width: 2000, height: 1200 },
canvasSize,
insets,
});
expect(next).toEqual({ scale: 1, x: 12, y: insets.top });
});
test('尺寸非法或画布未就绪时原样返回', () => {
const viewport = { x: 5, y: 6, scale: 1 };
expect(
revealResourceCanvasGenerationContent({
viewport,
content: { x: 0, y: 0, width: 0, height: 0 },
canvasSize,
insets,
}),
).toEqual(viewport);
expect(
revealResourceCanvasGenerationContent({
viewport,
content: { x: 0, y: 0, width: 10, height: 10 },
canvasSize: { width: 0, height: 0 },
insets,
}),
).toEqual(viewport);
});
});
describe('浮层顶边与高度按占位卡的真实屏幕位置算', () => {
const placement = (input: {
canvasHeight: number;
topInset: number;
bottomInset: number;
anchorTop: number;
anchorHeight: number;
}) => resolveResourceCanvasGenerationPanelPlacement(input);
test('空间够时挂在卡下面:顶边贴卡下沿,高度 = 卡下沿到安全底边', () => {
const result = placement({
canvasHeight: 568,
topInset: 54,
bottomInset: 84,
anchorTop: 54,
anchorHeight: 128,
});
expect(result.overlaysAnchor).toBe(false);
// 安全底边 484 - 卡下沿(54 + 128 - 间隙 12 = 290。
expect(result.top).toBe(54 + 128 + 12);
expect(result.availableHeight).toBe(290);
expect(result.top + result.availableHeight).toBe(568 - 84);
});
test('安全带给不出可编辑高度时改为盖住占位,拿「卡顶边到安全底边」', () => {
// 缩放 1.5:占位卡视觉 192px,卡下面只剩 234 (< 280) → 盖住占位。
const result = placement({
canvasHeight: 568,
topInset: 58,
bottomInset: 72,
anchorTop: 58,
anchorHeight: 192,
});
expect(result.overlaysAnchor).toBe(true);
expect(result.top).toBe(58);
// 568 - 72 - 58 = 438:远大于「一百多像素」的旧表现。
expect(result.availableHeight).toBe(438);
expect(result.availableHeight).toBeGreaterThanOrEqual(260);
});
/**
* 验收现场那条「生成窗口和画布边缘碰撞」的回归:占位卡是追加在栏目内容最下方的,
* 落到画布下半屏是常态。旧口径只吃「安全带高度 − 卡高」,等于假设卡永远贴在安全带上沿,
* 于是浮层底边越过画布下沿被 `overflow: hidden` 切掉。
*/
test('占位卡落到下半屏时浮层底边仍收在安全底边以内', () => {
const result = placement({
canvasHeight: 600,
topInset: 60,
bottomInset: 90,
anchorTop: 470,
anchorHeight: 128,
});
expect(result.overlaysAnchor).toBe(true);
// 卡顶边 470 会顶出安全底边(510 - 160 = 350),所以顶边先收到 350,再给一整段可编辑高度。
expect(result.top).toBe(350);
expect(result.top + result.availableHeight).toBeLessThanOrEqual(600 - 90);
// 中间位置同理:顶边贴卡顶,底边贴安全底边。
const middle = placement({
canvasHeight: 600,
topInset: 60,
bottomInset: 90,
anchorTop: 260,
anchorHeight: 128,
});
expect(middle.overlaysAnchor).toBe(true);
expect(middle.top).toBe(260);
expect(middle.top + middle.availableHeight).toBe(600 - 90);
});
test('空间充足时收到上限,几何非法时回退到最小编辑高度', () => {
expect(
placement({
canvasHeight: 1400,
topInset: 54,
bottomInset: 84,
anchorTop: 54,
anchorHeight: 128,
}).availableHeight,
).toBe(520);
expect(
placement({
canvasHeight: 0,
topInset: 54,
bottomInset: 84,
anchorTop: 54,
anchorHeight: 128,
}),
).toEqual({ overlaysAnchor: false, top: 54, availableHeight: 280 });
// 画布小到连安全带都装不下:退到安全带能给的量,但仍有可编辑高度。
const tiny = placement({
canvasHeight: 240,
topInset: 40,
bottomInset: 40,
anchorTop: 40,
anchorHeight: 128,
});
expect(tiny.overlaysAnchor).toBe(true);
expect(tiny.availableHeight).toBe(160);
expect(tiny.top + tiny.availableHeight).toBeLessThanOrEqual(240 - 40);
});
});
describe('浮层水平收口:贴着卡片,但恒定留在画布内', () => {
const canvasWidth = 900;
const panelWidth = resourceCanvasGenerationPanelWidth(canvasWidth);
test('宽度口径与浮层 CSS 同源:min(560, 画布宽 - 24)', () => {
expect(panelWidth).toBe(RESOURCE_CANVAS_GENERATION_PANEL_MAX_WIDTH);
expect(resourceCanvasGenerationPanelWidth(500)).toBe(
500 - RESOURCE_CANVAS_GENERATION_PANEL_VIEWPORT_INSET * 2,
);
// 画布未就绪(量不到宽)时退回最大宽度,交给 CSS 收。
expect(resourceCanvasGenerationPanelWidth(0)).toBe(
RESOURCE_CANVAS_GENERATION_PANEL_MAX_WIDTH,
);
});
test('卡片贴左边 / 贴右边时锚点收进来,浮层两边各留 12px', () => {
const left = clampResourceCanvasGenerationPanelAnchorX({
centerX: 20,
canvasWidth,
panelWidth,
});
expect(left - panelWidth / 2).toBe(
RESOURCE_CANVAS_GENERATION_PANEL_VIEWPORT_INSET,
);
const right = clampResourceCanvasGenerationPanelAnchorX({
centerX: canvasWidth - 5,
canvasWidth,
panelWidth,
});
expect(right + panelWidth / 2).toBe(
canvasWidth - RESOURCE_CANVAS_GENERATION_PANEL_VIEWPORT_INSET,
);
// 卡片在中间时不动:收口只在真的会越界时才生效。
expect(
clampResourceCanvasGenerationPanelAnchorX({
centerX: canvasWidth / 2,
canvasWidth,
panelWidth,
}),
).toBe(canvasWidth / 2);
});
test('画布比浮层还窄时居中(宽度收口交给 CSS),几何非法时原样返回', () => {
const narrowCanvas = 400;
const narrowPanel = resourceCanvasGenerationPanelWidth(narrowCanvas);
expect(
clampResourceCanvasGenerationPanelAnchorX({
centerX: 10,
canvasWidth: narrowCanvas,
panelWidth: narrowPanel,
}),
).toBe(narrowCanvas / 2);
expect(
clampResourceCanvasGenerationPanelAnchorX({
centerX: 33,
canvasWidth: Number.NaN,
panelWidth: 560,
}),
).toBe(33);
expect(
clampResourceCanvasGenerationPanelAnchorX({
centerX: 33,
canvasWidth: 800,
panelWidth: 0,
}),
).toBe(33);
});
});