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 记录两条排障口径:卡类手势必须只有一份阈值与点击抑制判据;画布浮层几何只能按真实屏幕位置算、分页锚点必须让开钉死的标题栏。
272 lines
9.0 KiB
TypeScript
272 lines
9.0 KiB
TypeScript
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);
|
||
});
|
||
});
|