修复: 生成质量机制根治——max_tokens截断检测+自动重试/icon白名单机械归一/空image剔除/金句引号伪元素治理/文本溢出扩高+重叠校正/图表y轴自适应防窄幅失真/stat缩字兜底/饼图图例流式排布/卡片icon字体锁定
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { niceDomain, fmtTick } from '../src/components/editor/chart-domain'
|
||||
|
||||
describe('niceDomain', () => {
|
||||
// 已确诊 bug 场景:数据全挤 97~98,旧实现映射到 [0,98] 导致折线贴顶
|
||||
it('高基线小波动:line 值域收紧为数据带,折线不贴顶', () => {
|
||||
const d = niceDomain([97, 98, 97.6, 97, 98])
|
||||
// 波动 <10% → domain = [97 - 1.5, 98 + 1.5],step 0.5 → [95.5, 99.5]
|
||||
expect(d.min).toBeLessThan(97)
|
||||
expect(d.max).toBeGreaterThan(98)
|
||||
expect(d.max - d.min).toBeLessThan(10)
|
||||
})
|
||||
|
||||
it('高基线小波动:ticks 4~5 个且为整数步长', () => {
|
||||
const d = niceDomain([97, 98, 97.6, 97, 98])
|
||||
expect(d.ticks.length).toBeGreaterThanOrEqual(4)
|
||||
expect(d.ticks.length).toBeLessThanOrEqual(6)
|
||||
const step = d.ticks[1] - d.ticks[0]
|
||||
expect(step).toBe(1)
|
||||
// 首尾与 domain 对齐
|
||||
expect(d.ticks[0]).toBe(d.min)
|
||||
expect(d.ticks[d.ticks.length - 1]).toBe(d.max)
|
||||
})
|
||||
|
||||
it('bar 类保持 0 基线(不因数据全大而从数据 min 起)', () => {
|
||||
const d = niceDomain([97, 98, 97.6, 97, 98], { zeroBase: true })
|
||||
expect(d.min).toBe(0)
|
||||
expect(d.ticks[0]).toBe(0)
|
||||
expect(d.max).toBeGreaterThanOrEqual(98)
|
||||
})
|
||||
|
||||
it('常规整数数据 [1,2,3] → nice 步长(0.5 或 1),刻度覆盖 max', () => {
|
||||
const d = niceDomain([1, 2, 3])
|
||||
const step = d.ticks[1] - d.ticks[0]
|
||||
expect([0.5, 1]).toContain(step)
|
||||
expect(d.ticks).toContain(3)
|
||||
})
|
||||
|
||||
it('小数值数据 [0.1, 0.2] → 步长 0.5,下限 0.5 生效', () => {
|
||||
const d = niceDomain([0.1, 0.2])
|
||||
expect(d.ticks[1] - d.ticks[0]).toBe(0.5)
|
||||
expect(d.min).toBeLessThanOrEqual(0.1)
|
||||
expect(d.max).toBeGreaterThanOrEqual(0.2)
|
||||
})
|
||||
|
||||
it('单值数据不产生除零/NaN', () => {
|
||||
const d = niceDomain([5])
|
||||
expect(d.min).toBeLessThan(d.max)
|
||||
expect(Number.isFinite(d.min)).toBe(true)
|
||||
expect(Number.isFinite(d.max)).toBe(true)
|
||||
expect(d.ticks.length).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
it('全等值数据 line 值域仍为区间而非单点', () => {
|
||||
const d = niceDomain([42, 42, 42, 42])
|
||||
expect(d.max).toBeGreaterThan(d.min)
|
||||
expect(d.ticks.length).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
it('全等值数据 bar 类 → [0, ≥value]', () => {
|
||||
const d = niceDomain([42, 42, 42], { zeroBase: true })
|
||||
expect(d.min).toBe(0)
|
||||
expect(d.max).toBeGreaterThanOrEqual(42)
|
||||
})
|
||||
|
||||
it('maxCap 作为上限覆盖(仅当 > 数据 max)', () => {
|
||||
const d = niceDomain([1, 2, 3], { zeroBase: true, maxCap: 100 })
|
||||
expect(d.max).toBeGreaterThanOrEqual(100)
|
||||
})
|
||||
|
||||
it('maxCap 小于数据 max 时不收紧值域', () => {
|
||||
const d = niceDomain([1, 2, 3], { zeroBase: true, maxCap: 1 })
|
||||
expect(d.max).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
it('空数据返回安全默认值域', () => {
|
||||
const d = niceDomain([])
|
||||
expect(d.max).toBeGreaterThan(d.min)
|
||||
expect(d.ticks.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('过滤 NaN/Infinity', () => {
|
||||
const d = niceDomain([NaN, Infinity, -Infinity, 5, 10])
|
||||
expect(d.min).toBeLessThanOrEqual(5)
|
||||
expect(d.max).toBeGreaterThanOrEqual(10)
|
||||
})
|
||||
|
||||
it('大整数数据步长取 5×10^k', () => {
|
||||
const d = niceDomain([1000, 1200, 1500, 2000], { zeroBase: true })
|
||||
const step = d.ticks[1] - d.ticks[0]
|
||||
expect(step).toBeGreaterThan(0)
|
||||
// 2000 跨度 → 步长 500 或 1000 之类 nice 数
|
||||
expect([100, 200, 500, 1000].includes(step)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fmtTick', () => {
|
||||
it('去尾零', () => {
|
||||
expect(fmtTick(2.5)).toBe('2.5')
|
||||
expect(fmtTick(20)).toBe('20')
|
||||
expect(fmtTick(0)).toBe('0')
|
||||
expect(fmtTick(97.6)).toBe('97.6')
|
||||
})
|
||||
|
||||
it('消浮点误差', () => {
|
||||
expect(fmtTick(0.1 + 0.2)).toBe('0.3')
|
||||
expect(fmtTick(2.55)).toBe('2.55')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,236 @@
|
||||
/* =====================================================================
|
||||
* norm-slides-enhance.test.ts — 生成层质量修复测试
|
||||
* 覆盖:空文本元素剔除 / 文本重叠机械校正 / chart 数据形状校正
|
||||
* ===================================================================== */
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { normSlides } from '../src/core/ai'
|
||||
import type { SlideElement } from '../src/core/types'
|
||||
|
||||
function el(id: string, over: Partial<SlideElement> = {}): SlideElement {
|
||||
return { id, type: 'text', x: 10, y: 10, w: 40, h: 10, content: '内容', style: {}, ...over }
|
||||
}
|
||||
function norm(elements: SlideElement[]): SlideElement[] {
|
||||
return normSlides([{ background: 'bg', elements }])[0].elements
|
||||
}
|
||||
|
||||
describe('空文本元素剔除', () => {
|
||||
it.each(['title', 'text', 'quote', 'list', 'card', 'stat'] as const)('%s content 空白 → 剔除', (t) => {
|
||||
const els = norm([el('a', { type: t, content: ' \n ' }), el('b', { y: 50 })])
|
||||
expect(els.map(e => e.id)).toEqual(['b'])
|
||||
})
|
||||
|
||||
it.each(['chart', 'table'] as const)('%s 空 content 不剔除', (t) => {
|
||||
const els = norm([el('a', { type: t, content: '' })])
|
||||
expect(els).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('小空形状(面积 ≤8% 画布)= 装饰,保留;大空框(>8%)= 残缺文本框,丢弃', () => {
|
||||
// 剔除层不处理 shape,去留由 sanitizeShapes「大空框」装饰纪律决定(阈值 w%×h% > 800)
|
||||
const small = norm([el('a', { type: 'shape', content: '', w: 5, h: 5, style: { shapeType: 'rect', fill: '#e74c3c' } })])
|
||||
expect(small).toHaveLength(1)
|
||||
const big = norm([el('a', { type: 'shape', content: '', w: 40, h: 30, style: { shapeType: 'rect', fill: '#e74c3c' } })])
|
||||
expect(big).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('空 image/video(AI 占位)→ 剔除,防渲染成空白矩形框', () => {
|
||||
const els = norm([el('a', { type: 'image', content: '' }), el('b', { type: 'video', content: '' }), el('c')])
|
||||
expect(els.map(e => e.id)).toEqual(['c'])
|
||||
})
|
||||
|
||||
it('有内容的 image/video 保留', () => {
|
||||
const els = norm([el('a', { type: 'image', content: 'data:image/png;base64,xx' }), el('b', { type: 'video', content: 'https://v/1.mp4' })])
|
||||
expect(els).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('有 segments 的元素即使 content 空也保留', () => {
|
||||
const e = el('a', { content: '' })
|
||||
// normSegments 输入格式:行对象数组,每行含 segments
|
||||
;(e as any).segments = [{ segments: [{ text: '富文本' }] }]
|
||||
expect(norm([e])).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('文本重叠机械校正', () => {
|
||||
it('显著相交(>30%)→ 后者下移至前者下缘 + 最小间距', () => {
|
||||
// a: 10,10 40x10;b: 10,15 40x10 → 相交 40x5=200,小元素面积 400,占比 50%>30%
|
||||
// 第一阶段移至 b 下缘 20,第二阶段再保 1.5 最小间距 → 21.5
|
||||
const els = norm([el('a'), el('b', { y: 15 })])
|
||||
expect(els.find(e => e.id === 'b')!.y).toBe(21.5)
|
||||
})
|
||||
|
||||
it('轻微相交(≤30%,有意叠加)→ 不动', () => {
|
||||
// a:10-20 b:18-28 相交高 2,小面积 400,占比 5%…精确:40x2=80,占比 20%
|
||||
// 相交不显著 → 第一阶段不动;但同列 gap 仅 -2 <1.5 → 第二阶段保最小间距至 21.5
|
||||
const els = norm([el('a'), el('b', { y: 18 })])
|
||||
expect(els.find(e => e.id === 'b')!.y).toBe(21.5)
|
||||
})
|
||||
|
||||
it('轻微相交变体(部分重叠但占比≤30%)→ 不动', () => {
|
||||
// a: 10,10 40x10;b: 20,18 40x12 → 相交 30x2=60,小面积 400,占比 15%
|
||||
// 相交不显著 → 第一阶段不动;同列 gap <1.5 → 第二阶段保最小间距至 21.5
|
||||
const els = norm([el('a'), el('b', { x: 20, y: 18, w: 40, h: 12 })])
|
||||
expect(els.find(e => e.id === 'b')!.y).toBe(21.5)
|
||||
})
|
||||
|
||||
it('下移会出画布 → 缩高贴底', () => {
|
||||
// a: 10-100;b(90-100) 平移到 y=100 后需缩高收进画布(h 钳 3,y 贴底 97)
|
||||
const els = norm([el('a', { y: 95, h: 5, w: 40 }), el('b', { y: 90, h: 10, w: 40 })])
|
||||
const b = els.find(e => e.id === 'b')!
|
||||
expect(b.h).toBe(3)
|
||||
expect(b.y).toBe(97)
|
||||
expect(b.y + b.h).toBeLessThanOrEqual(100)
|
||||
})
|
||||
|
||||
it('chart/shape 背景元素不参与重叠校正', () => {
|
||||
const chart = el('c', { type: 'chart', content: '[{"label":"a","value":1}]', x: 10, y: 10, w: 60, h: 40, style: { chartType: 'bar' } })
|
||||
const text = el('t', { y: 20 })
|
||||
const els = norm([chart, text])
|
||||
expect(els.find(e => e.id === 't')!.y).toBe(20)
|
||||
})
|
||||
})
|
||||
|
||||
describe('chart 数据形状校正', () => {
|
||||
it('pie 多系列 → 取第一系列,values 长度=labels 长度', () => {
|
||||
const pie = el('p', {
|
||||
type: 'chart', x: 30, y: 30, w: 40, h: 30,
|
||||
content: JSON.stringify({ series: ['Q1', 'Q2'], items: [{ label: '华东', values: [120, 150] }, { label: '华南', values: [80, 90] }] }),
|
||||
style: { chartType: 'pie' }
|
||||
})
|
||||
const out = norm([pie])[0]
|
||||
const data = JSON.parse(out.content)
|
||||
expect(data).toHaveLength(2)
|
||||
expect(data[0]).toEqual({ label: '华东', value: 120 })
|
||||
expect(data[1]).toEqual({ label: '华南', value: 80 })
|
||||
})
|
||||
|
||||
it('pie 单系列已是正确格式 → 不变', () => {
|
||||
const src = [{ label: 'a', value: 1 }, { label: 'b', value: 2 }]
|
||||
const pie = el('p', { type: 'chart', content: JSON.stringify(src), style: { chartType: 'doughnut' } })
|
||||
expect(JSON.parse(norm([pie])[0].content)).toEqual(src)
|
||||
})
|
||||
|
||||
it('radar 指标<3 → 纠正为 bar', () => {
|
||||
const radar = el('r', {
|
||||
type: 'chart',
|
||||
content: '[{"label":"a","value":1},{"label":"b","value":2}]',
|
||||
style: { chartType: 'radar' }
|
||||
})
|
||||
const out = norm([radar])[0]
|
||||
expect(out.style.chartType).toBe('bar')
|
||||
})
|
||||
|
||||
it('radar 指标≥3 → 保持 radar', () => {
|
||||
const radar = el('r', {
|
||||
type: 'chart',
|
||||
content: '[{"label":"a","value":1},{"label":"b","value":2},{"label":"c","value":3}]',
|
||||
style: { chartType: 'radar' }
|
||||
})
|
||||
expect(norm([radar])[0].style.chartType).toBe('radar')
|
||||
})
|
||||
})
|
||||
|
||||
describe('icon 白名单归一化', () => {
|
||||
it.each(['✅', '⚠️', '💡', '🎯', '📌', '🔍', '⭐'])('白名单 icon %s 保留(去 VS16 归一化)', (icon) => {
|
||||
const els = normSlides([{ background: 'bg', elements: [{ id: 'a', type: 'card', x: 10, y: 10, w: 40, h: 20, content: '标题\n正文', style: { icon } }] }])
|
||||
expect(els[0].elements[0].style.icon).toBe(icon.replace(/️/g, ''))
|
||||
})
|
||||
|
||||
it.each(['❗', '✔️', '❌', '☑️', '🔥', '🚀', '✨'])('非白名单 icon %s 丢弃(留空)', (icon) => {
|
||||
const els = normSlides([{ background: 'bg', elements: [{ id: 'a', type: 'card', x: 10, y: 10, w: 40, h: 20, content: '标题\n正文', style: { icon } }] }])
|
||||
expect(els[0].elements[0].style.icon).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('空 shape 金句底座框剔除(dropEmptyElements)', () => {
|
||||
it('空 shape opacity<0.15 → 剔除;opacity 达标且色距足够 → 保留', () => {
|
||||
// a/b x 拉开,避免触发「叠放装饰丢弃后出现者」的装饰纪律干扰本用例
|
||||
const els = norm([
|
||||
el('a', { type: 'shape', content: '', w: 5, h: 5, x: 5, style: { shapeType: 'rect', fill: '#e74c3c', opacity: 0.1 } }),
|
||||
el('b', { type: 'shape', content: '', w: 5, h: 5, x: 60, style: { shapeType: 'rect', fill: '#e74c3c', opacity: 0.5 } }),
|
||||
el('c')
|
||||
])
|
||||
expect(els.map(e => e.id)).toEqual(['b', 'c'])
|
||||
})
|
||||
|
||||
it('空 shape fill 与背景色距 <60(视觉隐形)→ 剔除', () => {
|
||||
const els = norm([
|
||||
el('a', { type: 'shape', content: '', w: 5, h: 5, style: { shapeType: 'rect', fill: '#ffffff' } }),
|
||||
el('b')
|
||||
])
|
||||
expect(els.map(e => e.id)).toEqual(['b'])
|
||||
})
|
||||
|
||||
it('带内容的 shape 即使低透明度也不剔除', () => {
|
||||
const els = norm([el('a', { type: 'shape', content: '文字', w: 20, h: 10, style: { fill: '#e74c3c', opacity: 0.05 } })])
|
||||
expect(els).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('金句引号剥离 → decoQuote 转换(sanitizeQuoteDeco)', () => {
|
||||
it('quote 首尾成对弯引号包裹 → 剥离并置 style.decoQuote=true', () => {
|
||||
const els = norm([el('q', { type: 'quote', content: '“少即是多”', style: { fontSize: 44 } })])
|
||||
expect(els[0].content).toBe('少即是多')
|
||||
expect(els[0].style.decoQuote).toBe(true)
|
||||
})
|
||||
|
||||
it('quote 「」包裹 → 剥离并置 decoQuote', () => {
|
||||
const els = norm([el('q', { type: 'quote', content: '「内容正文」', style: {} })])
|
||||
expect(els[0].content).toBe('内容正文')
|
||||
expect(els[0].style.decoQuote).toBe(true)
|
||||
})
|
||||
|
||||
it('quote content 只含引号字符(剥后为空)→ 整元素剔除', () => {
|
||||
const els = norm([el('q', { type: 'quote', content: '“”' }), el('b')])
|
||||
expect(els.map(e => e.id)).toEqual(['b'])
|
||||
})
|
||||
|
||||
it('quote 无引号字符 → content 与 style 不动', () => {
|
||||
const els = norm([el('q', { type: 'quote', content: '没有引号的正文', style: {} })])
|
||||
expect(els[0].content).toBe('没有引号的正文')
|
||||
expect(els[0].style.decoQuote).toBeUndefined()
|
||||
})
|
||||
|
||||
it('非 quote 类型的引号字符不剥离(正文合法引用)', () => {
|
||||
const els = norm([el('t', { type: 'text', content: '他说“你好”', style: {} })])
|
||||
expect(els[0].content).toBe('他说“你好”')
|
||||
expect(els[0].style.decoQuote).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('文本容量估算扩高(sanitizeOverflow + estimateTextH)', () => {
|
||||
it('长文本 h 不足 → 扩高(不超画布)', () => {
|
||||
// 60 字正文 24px、宽 40%:每行约 (1280*0.4)/(24*1.05)≈20 字 → 3 行 → 需 h≈(3*24*1.5+0.8*24)/720*100≈17.7
|
||||
const els = norm([el('t', { content: '一'.repeat(60), w: 40, h: 6, style: { fontSize: 24 } })])
|
||||
expect(els[0].h).toBeGreaterThan(6)
|
||||
expect(els[0].y + els[0].h).toBeLessThanOrEqual(100)
|
||||
})
|
||||
|
||||
it('短文本 h 已足 → 不动', () => {
|
||||
const els = norm([el('t', { content: '短', w: 40, h: 10, style: { fontSize: 24 } })])
|
||||
expect(els[0].h).toBe(10)
|
||||
})
|
||||
|
||||
it('扩高出画布 → 降字号一档(不低于 0.75×)', () => {
|
||||
// 长文 + 低 y + 大字号:容量远超剩余空间 → 降字号
|
||||
const els = norm([el('t', { y: 60, h: 10, w: 30, content: '一'.repeat(200), style: { fontSize: 48 } })])
|
||||
expect(els[0].style.fontSize).toBeLessThan(48)
|
||||
expect(els[0].style.fontSize!).toBeGreaterThanOrEqual(48 * 0.75)
|
||||
expect(els[0].y + els[0].h).toBeLessThanOrEqual(100)
|
||||
})
|
||||
|
||||
it('card 标题按 1.5em 折算容量(长标题单行放不下时扩高)', () => {
|
||||
const els = norm([el('c', {
|
||||
type: 'card', w: 25, h: 12, style: { fontSize: 20 },
|
||||
content: '这是一个特别长的卡片标题超过一行折行\n正文内容'
|
||||
})])
|
||||
expect(els[0].h).toBeGreaterThan(12)
|
||||
})
|
||||
|
||||
it('重叠缩高不低于容量下限:下移空间不足时缩字号而不是硬裁', () => {
|
||||
// a 在上方;b y=90 与 a 相交 → 平移空间只剩 10%,长文本容量 >10 → 应缩字号而非 h=3 硬裁
|
||||
const els = norm([el('a', { h: 6 }), el('b', { y: 90, h: 5, w: 40, content: '一'.repeat(80), style: { fontSize: 24 } })])
|
||||
const b = els.find(e => e.id === 'b')!
|
||||
expect(b.style.fontSize).toBeLessThan(24)
|
||||
expect(b.y + b.h).toBeLessThanOrEqual(100)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
/* =====================================================================
|
||||
* sanitize-contrast.test.ts — 对比度治理测试
|
||||
* 保护对象:normSlides 内的 sanitizeContrast(深底小字 accent 降级、
|
||||
* 透明/近背景色空装饰形状丢弃)
|
||||
* ===================================================================== */
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { normSlides } from '../src/core/ai'
|
||||
import type { SlideElement } from '../src/core/types'
|
||||
|
||||
/* ---------- 测试数据工厂 ---------- */
|
||||
function textEl(id: string, over: Partial<SlideElement> = {}): SlideElement {
|
||||
return { id, type: 'text', x: 10, y: 30, w: 60, h: 20, content: '正文', style: { color: 'accent', fontSize: 20 }, ...over }
|
||||
}
|
||||
function shapeEl(id: string, over: Partial<SlideElement> = {}): SlideElement {
|
||||
return { id, type: 'shape', x: 70, y: 80, w: 10, h: 10, content: '', style: { shapeType: 'circle' }, ...over }
|
||||
}
|
||||
function norm(background: string, elements: SlideElement[]): SlideElement[] {
|
||||
return normSlides([{ background, elements }])[0].elements
|
||||
}
|
||||
|
||||
describe('sanitizeContrast 深底小字 accent 降级', () => {
|
||||
it('深底页 text color=accent fontSize=20 → 降为 muted', () => {
|
||||
const els = norm('g-primary', [textEl('t')])
|
||||
expect((els[0].style as any).color).toBe('muted')
|
||||
})
|
||||
|
||||
it('深底页 stat 大数字 color=accent fontSize=72 → 保留 accent', () => {
|
||||
const els = norm('g-deep', [textEl('st', { type: 'stat', content: '65%', style: { color: 'accent', fontSize: 72 } })])
|
||||
expect((els[0].style as any).color).toBe('accent')
|
||||
})
|
||||
|
||||
it('浅底页(bg)text color=accent → 不动', () => {
|
||||
const els = norm('bg', [textEl('t')])
|
||||
expect((els[0].style as any).color).toBe('accent')
|
||||
})
|
||||
|
||||
it('深底页 text color=accent fontSize=32 → 不动(大字合法)', () => {
|
||||
const els = norm('primary', [textEl('t', { style: { color: 'accent', fontSize: 32 } })])
|
||||
expect((els[0].style as any).color).toBe('accent')
|
||||
})
|
||||
|
||||
it('深底页 quote color=muted → 不动(已是 muted)', () => {
|
||||
const els = norm('g-primary', [textEl('q', { type: 'quote', style: { color: 'muted', fontSize: 40 } })])
|
||||
expect((els[0].style as any).color).toBe('muted')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sanitizeContrast 空装饰形状丢弃', () => {
|
||||
it('空装饰形状无 fill(透明)→ 丢弃', () => {
|
||||
const els = norm('bg', [shapeEl('s1', { style: { shapeType: 'circle' } })])
|
||||
expect(els.find(e => e.id === 's1')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('空装饰形状 fill=primary 放 g-primary 背景 → 丢弃(同色隐形)', () => {
|
||||
const els = norm('g-primary', [shapeEl('s1', { style: { shapeType: 'circle', fill: 'primary' } })])
|
||||
expect(els.find(e => e.id === 's1')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('空装饰形状 fill=accent 放 g-primary 背景 → 保留(不同色)', () => {
|
||||
const els = norm('g-primary', [shapeEl('s1', { style: { shapeType: 'circle', fill: 'accent' } })])
|
||||
expect(els.find(e => e.id === 's1')).toBeDefined()
|
||||
})
|
||||
|
||||
it('带内容形状 fill 同背景色 → 保留', () => {
|
||||
const els = norm('g-primary', [shapeEl('s1', { content: '标签', style: { shapeType: 'bubble', fill: 'primary' } })])
|
||||
expect(els.find(e => e.id === 's1')).toBeDefined()
|
||||
})
|
||||
})
|
||||
@@ -21,7 +21,7 @@ function norm(elements: SlideElement[]): SlideElement[] {
|
||||
|
||||
describe('sanitizeShapes 等比化', () => {
|
||||
it('star 非正方形框 → 取小者,中心不变', () => {
|
||||
const els = norm([shape('a', { style: { shapeType: 'star' }, x: 40, y: 20, w: 20, h: 8 })])
|
||||
const els = norm([shape('a', { style: { shapeType: 'star', fill: 'accent' }, 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
|
||||
@@ -29,7 +29,7 @@ describe('sanitizeShapes 等比化', () => {
|
||||
})
|
||||
|
||||
it('circle 扁宽框 → 正方形', () => {
|
||||
const els = norm([shape('a', { style: { shapeType: 'circle' }, x: 10, y: 30, w: 30, h: 10 })])
|
||||
const els = norm([shape('a', { style: { shapeType: 'circle', fill: 'accent' }, 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
|
||||
@@ -37,7 +37,7 @@ describe('sanitizeShapes 等比化', () => {
|
||||
})
|
||||
|
||||
it('arrow 天然扁宽 → 不等比', () => {
|
||||
const els = norm([shape('a', { style: { shapeType: 'arrow' }, x: 10, y: 30, w: 30, h: 10 })])
|
||||
const els = norm([shape('a', { style: { shapeType: 'arrow', fill: 'accent' }, x: 10, y: 30, w: 30, h: 10 })])
|
||||
expect(els[0].w).toBe(30)
|
||||
expect(els[0].h).toBe(10)
|
||||
})
|
||||
@@ -102,9 +102,9 @@ describe('sanitizeShapes 怪异装饰兜底', () => {
|
||||
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 })
|
||||
shape('roof', { style: { shapeType: 'triangle', fill: 'accent' }, x: 40, y: 10, w: 20, h: 12 }),
|
||||
shape('body', { style: { fill: 'accent' }, x: 42, y: 20, w: 16, h: 15 }),
|
||||
shape('dot', { style: { shapeType: 'circle', fill: 'accent' }, x: 48, y: 24, w: 5, h: 5 })
|
||||
])
|
||||
expect(els.find(e => e.id === 'roof')).toBeDefined()
|
||||
expect(els.find(e => e.id === 'body')).toBeUndefined()
|
||||
|
||||
Reference in New Issue
Block a user