返回 oh-my-ppt
build-insert-element-html.test.ts
根目录 / tests / unit / editor / build-insert-element-html.test.ts
1 import { describe, expect, it } from 'vitest'
2 import {
3 buildIconElementHtml,
4 buildShapeElementHtml,
5 isValidBlockId,
6 isValidColor,
7 type InsertShapeType
8 } from '../../../src/renderer/src/components/session-detail/workspace/insert-shapes'
9
10 const VALID_BLOCK_ID = 'select-arcsin1-abcd1234'
11 const LAYOUT = { left: 100, top: 200, width: 240, height: 120, zIndex: 12 }
12
13 describe('buildShapeElementHtml', () => {
14 const types: InsertShapeType[] = [
15 'rect',
16 'rounded-rect',
17 'ellipse',
18 'triangle',
19 'diamond',
20 'pentagon',
21 'hexagon',
22 'parallelogram',
23 'trapezoid',
24 'star-5',
25 'line',
26 'arrow-right',
27 'chevron-right'
28 ]
29
30 types.forEach((type) => {
31 it(`produces a selectable shape fragment for "${type}"`, () => {
32 const html = buildShapeElementHtml({ ...LAYOUT, blockId: VALID_BLOCK_ID, type })
33 expect(html).toContain(`data-block-id="${VALID_BLOCK_ID}"`)
34 expect(html).toContain('data-ppt-edit-kind="shape"')
35 expect(html).toContain(`data-ppt-shape-type="${type}"`)
36 expect(html).toContain('position:absolute')
37 expect(html).toContain('left:100px')
38 expect(html).toContain('top:200px')
39 expect(html).toContain('width:240px')
40 expect(html).toContain('height:120px')
41 expect(html).toContain('z-index:12')
42 expect(html).toContain('<svg')
43 expect(html).toContain('</svg>')
44 })
45 })
46
47 it('line uses non-scaling-stroke so stroke width stays stable when stretched', () => {
48 const line = buildShapeElementHtml({ ...LAYOUT, blockId: VALID_BLOCK_ID, type: 'line' })
49 expect(line).toContain('vector-effect="non-scaling-stroke"')
50 })
51
52 it('arrow-right is a solid filled block arrow (not a thin stroked path)', () => {
53 const arrow = buildShapeElementHtml({
54 ...LAYOUT,
55 blockId: VALID_BLOCK_ID,
56 type: 'arrow-right'
57 })
58 expect(arrow).toContain('<polygon')
59 // filled block arrow must not be stroke-only
60 expect(arrow).toMatch(/fill="#[0-9a-fA-F]+"/)
61 })
62
63 it('polygon-based shapes render a <polygon> element', () => {
64 const polygonShapes: InsertShapeType[] = [
65 'triangle',
66 'diamond',
67 'pentagon',
68 'hexagon',
69 'parallelogram',
70 'trapezoid',
71 'star-5',
72 'chevron-right'
73 ]
74 polygonShapes.forEach((type) => {
75 const html = buildShapeElementHtml({ ...LAYOUT, blockId: VALID_BLOCK_ID, type })
76 expect(html).toContain('<polygon')
77 })
78 })
79
80 it('rejects an invalid blockId', () => {
81 expect(() =>
82 buildShapeElementHtml({ ...LAYOUT, blockId: 'evil"><script>', type: 'rect' })
83 ).toThrow(/invalid blockId/)
84 })
85
86 it('rejects an unknown shape type', () => {
87 expect(() =>
88 buildShapeElementHtml({
89 ...LAYOUT,
90 blockId: VALID_BLOCK_ID,
91 type: 'definitely-not-real' as InsertShapeType
92 })
93 ).toThrow(/unknown shape type/)
94 })
95
96 it('rejects a color that would break out of the attribute', () => {
97 expect(() =>
98 buildShapeElementHtml({
99 ...LAYOUT,
100 blockId: VALID_BLOCK_ID,
101 type: 'rect',
102 fill: 'red"/><script>alert(1)</script><rect x="'
103 })
104 ).toThrow(/invalid color/)
105 })
106
107 it('never emits script tags or inline event handlers', () => {
108 const html = buildShapeElementHtml({ ...LAYOUT, blockId: VALID_BLOCK_ID, type: 'rect' })
109 expect(html).not.toMatch(/<script/i)
110 expect(html).not.toMatch(/on\w+\s*=/i)
111 expect(html).not.toMatch(/javascript:/i)
112 })
113 })
114
115 describe('buildIconElementHtml', () => {
116 it('produces a selectable icon fragment with currentColor stroke', () => {
117 const html = buildIconElementHtml({ ...LAYOUT, blockId: VALID_BLOCK_ID, iconId: 'lightbulb' })
118 expect(html).toContain(`data-block-id="${VALID_BLOCK_ID}"`)
119 expect(html).toContain('data-ppt-edit-kind="icon"')
120 expect(html).toContain('data-ppt-icon-id="lightbulb"')
121 expect(html).toContain('stroke="currentColor"')
122 expect(html).toContain('color:#3f4b35')
123 expect(html).toContain('<svg')
124 })
125
126 it('numbered badge renders a solid filled circle with the number', () => {
127 const html = buildIconElementHtml({
128 ...LAYOUT,
129 blockId: VALID_BLOCK_ID,
130 iconId: 'number-3'
131 })
132 expect(html).toContain('data-ppt-icon-id="number-3"')
133 expect(html).toContain('<circle')
134 expect(html).toContain('fill="currentColor"')
135 expect(html).toContain('>3</')
136 // badge must not carry the stroke-icon attrs on the outer svg
137 expect(html).not.toMatch(/svg[^>]*stroke="currentColor"/)
138 })
139
140 it('rejects an unknown iconId', () => {
141 expect(() =>
142 buildIconElementHtml({ ...LAYOUT, blockId: VALID_BLOCK_ID, iconId: 'definitely-not-real' })
143 ).toThrow(/unknown iconId/)
144 })
145
146 it('serializes circle/line/rect nodes correctly', () => {
147 const html = buildIconElementHtml({ ...LAYOUT, blockId: VALID_BLOCK_ID, iconId: 'target' })
148 // target icon has 3 nested circles
149 const circleMatches = html.match(/<circle /g)
150 expect(circleMatches).not.toBeNull()
151 expect(circleMatches!.length).toBe(3)
152 })
153
154 it('never emits script tags or inline event handlers', () => {
155 const html = buildIconElementHtml({ ...LAYOUT, blockId: VALID_BLOCK_ID, iconId: 'star' })
156 expect(html).not.toMatch(/<script/i)
157 expect(html).not.toMatch(/on\w+\s*=/i)
158 expect(html).not.toMatch(/javascript:/i)
159 expect(html).not.toMatch(/<foreignObject/i)
160 })
161 })
162
163 describe('isValidBlockId / isValidColor', () => {
164 it('accepts the canonical generated id shape', () => {
165 expect(isValidBlockId('select-arcsin1-abcd1234')).toBe(true)
166 expect(isValidBlockId('select-arcsin1-Xy_9-AB12')).toBe(true)
167 })
168
169 it('rejects anything that is not the generated id shape', () => {
170 expect(isValidBlockId('')).toBe(false)
171 expect(isValidBlockId('foo')).toBe(false)
172 expect(isValidBlockId('"><script>')).toBe(false)
173 })
174
175 it('accepts hex and named colors, rejects expressions', () => {
176 expect(isValidColor('#abc')).toBe(true)
177 expect(isValidColor('#aabbcc')).toBe(true)
178 expect(isValidColor('red')).toBe(true)
179 expect(isValidColor('transparent')).toBe(true)
180 expect(isValidColor('currentColor')).toBe(true)
181 expect(isValidColor('url(foo)')).toBe(false)
182 expect(isValidColor('expression(alert(1))')).toBe(false)
183 })
184 })
185
185 lines TYPESCRIPT