测试: sanitizeShapes 形状校正覆盖(等比化/收边/防压字/拼图案/分隔条/数量兜底)
This commit is contained in:
@@ -0,0 +1,137 @@
|
|||||||
|
/* =====================================================================
|
||||||
|
* sanitize-shapes.test.ts — AI 输出形状校正测试
|
||||||
|
* 保护对象:normSlides 内的 sanitizeShapes(等比化/收边/防压字)
|
||||||
|
* AI 坐标自由输出,这些校正防止 clip-path 多边形变形、形状裁切、装饰压字
|
||||||
|
* ===================================================================== */
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { normSlides } from '../src/core/ai'
|
||||||
|
import type { SlideElement } from '../src/core/types'
|
||||||
|
|
||||||
|
/* ---------- 测试数据工厂 ---------- */
|
||||||
|
function shape(id: string, over: Partial<SlideElement> = {}): SlideElement {
|
||||||
|
return {
|
||||||
|
id, type: 'shape', x: 10, y: 10, w: 20, h: 8,
|
||||||
|
content: '', style: { shapeType: 'rect', fill: 'accent' },
|
||||||
|
...over
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function norm(elements: SlideElement[]): SlideElement[] {
|
||||||
|
return normSlides([{ background: 'bg', elements }])[0].elements
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('sanitizeShapes 等比化', () => {
|
||||||
|
it('star 非正方形框 → 取小者,中心不变', () => {
|
||||||
|
const els = norm([shape('a', { style: { shapeType: 'star' }, x: 40, y: 20, w: 20, h: 8 })])
|
||||||
|
expect(els[0].w).toBe(8)
|
||||||
|
expect(els[0].h).toBe(8)
|
||||||
|
expect(els[0].x).toBe(46) // 40 + (20-8)/2
|
||||||
|
expect(els[0].y).toBe(20)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('circle 扁宽框 → 正方形', () => {
|
||||||
|
const els = norm([shape('a', { style: { shapeType: 'circle' }, x: 10, y: 30, w: 30, h: 10 })])
|
||||||
|
expect(els[0].w).toBe(10)
|
||||||
|
expect(els[0].h).toBe(10)
|
||||||
|
expect(els[0].x).toBe(20) // 10 + 10
|
||||||
|
expect(els[0].y).toBe(30)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('arrow 天然扁宽 → 不等比', () => {
|
||||||
|
const els = norm([shape('a', { style: { shapeType: 'arrow' }, x: 10, y: 30, w: 30, h: 10 })])
|
||||||
|
expect(els[0].w).toBe(30)
|
||||||
|
expect(els[0].h).toBe(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sanitizeShapes 收进画布', () => {
|
||||||
|
it('形状超出下缘 → 收进', () => {
|
||||||
|
const els = norm([shape('a', { x: 10, y: 95, w: 30, h: 15 })])
|
||||||
|
expect(els[0].y).toBe(85)
|
||||||
|
expect(els[0].y + els[0].h).toBeLessThanOrEqual(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('形状超出右缘 → 收进', () => {
|
||||||
|
const els = norm([shape('a', { x: 90, y: 10, w: 30, h: 8 })])
|
||||||
|
expect(els[0].x).toBe(70)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sanitizeShapes 防压字', () => {
|
||||||
|
const title: SlideElement = {
|
||||||
|
id: 't', type: 'title', x: 10, y: 8, w: 80, h: 10, content: '标题', style: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('空胶囊压在 title 中部 → 移到 title 正下方', () => {
|
||||||
|
const els = norm([title, shape('cap', { x: 10, y: 12, w: 40, h: 3 })])
|
||||||
|
const cap = els.find(e => e.id === 'cap')!
|
||||||
|
expect(cap.y).toBe(19) // title.y + title.h + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
it('空胶囊同时压 title 与 list 两个元素 → 丢弃', () => {
|
||||||
|
const list: SlideElement = { id: 'l', type: 'list', x: 10, y: 14, w: 80, h: 40, content: '条目', style: {} }
|
||||||
|
// cap y=10 h=10:与 title 相交高 10 > 5,与 list(y14起) 相交高 6 > 5 → 命中两个
|
||||||
|
const els = norm([title, list, shape('cap', { x: 10, y: 10, w: 40, h: 10 })])
|
||||||
|
expect(els.find(e => e.id === 'cap')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('有文字的形状压字 → 不动', () => {
|
||||||
|
const els = norm([title, shape('bad', { x: 10, y: 12, w: 40, h: 3, content: '标签' })])
|
||||||
|
const bad = els.find(e => e.id === 'bad')!
|
||||||
|
expect(bad.y).toBe(12)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('贴标题下缘的合法胶囊(相交浅)→ 不误伤', () => {
|
||||||
|
// cap y=16 h=4(至20),title 至 y=18,相交高 2 = h*0.5,不满足「>」判定 → 保持原位
|
||||||
|
const els = norm([title, shape('cap', { x: 10, y: 16, w: 40, h: 4 })])
|
||||||
|
const cap = els.find(e => e.id === 'cap')!
|
||||||
|
expect(cap.y).toBe(16)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sanitizeShapes 怪异装饰兜底', () => {
|
||||||
|
it('横贯页面的细矩形(分隔线/进度条)→ 丢弃', () => {
|
||||||
|
const els = norm([shape('div', { x: 10, y: 50, w: 80, h: 3 })])
|
||||||
|
expect(els.find(e => e.id === 'div')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('扁宽但不到横贯程度 → 保留', () => {
|
||||||
|
const els = norm([shape('wide', { x: 10, y: 50, w: 50, h: 3 })])
|
||||||
|
expect(els.find(e => e.id === 'wide')).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('多形状叠放拼图案(三角+矩形+圆拼「房子」)→ 只保留先出现的', () => {
|
||||||
|
// 三角在上方,矩形/圆与其叠放 → 后两者丢弃
|
||||||
|
const els = norm([
|
||||||
|
shape('roof', { style: { shapeType: 'triangle' }, x: 40, y: 10, w: 20, h: 12 }),
|
||||||
|
shape('body', { x: 42, y: 20, w: 16, h: 15 }),
|
||||||
|
shape('dot', { style: { shapeType: 'circle' }, x: 48, y: 24, w: 5, h: 5 })
|
||||||
|
])
|
||||||
|
expect(els.find(e => e.id === 'roof')).toBeDefined()
|
||||||
|
expect(els.find(e => e.id === 'body')).toBeUndefined()
|
||||||
|
expect(els.find(e => e.id === 'dot')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('不相交的独立装饰 → 互不影响', () => {
|
||||||
|
const els = norm([
|
||||||
|
shape('a', { x: 5, y: 10, w: 10, h: 8 }),
|
||||||
|
shape('b', { x: 60, y: 60, w: 10, h: 8 })
|
||||||
|
])
|
||||||
|
expect(els.find(e => e.id === 'a')).toBeDefined()
|
||||||
|
expect(els.find(e => e.id === 'b')).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('空装饰超过 5 个 → 只保留前 5 个', () => {
|
||||||
|
// 间距拉开互不叠放(w/h 最小钳制为 3,间隔须 >3),纯验数量兜底
|
||||||
|
const els = norm(Array.from({ length: 7 }, (_, i) => shape('d' + i, { x: i * 5, y: 50, w: 3, h: 3 })))
|
||||||
|
expect(els.filter(e => e.type === 'shape').length).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('有文字的形状(bubble 标签)→ 不参与丢弃', () => {
|
||||||
|
const els = norm([
|
||||||
|
shape('tag1', { x: 10, y: 30, w: 15, h: 6, content: '要点' }),
|
||||||
|
shape('tag2', { x: 12, y: 32, w: 15, h: 6, content: '标签' })
|
||||||
|
])
|
||||||
|
expect(els.find(e => e.id === 'tag1')).toBeDefined()
|
||||||
|
expect(els.find(e => e.id === 'tag2')).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user