返回 oh-my-ppt
element-anchor.test.ts
根目录 / tests / unit / editor / element-anchor.test.ts
1 import { describe, expect, it } from 'vitest'
2
3 import {
4 ensureElementAnchorInHtml,
5 patchDraggedElementStyle,
6 patchGenericElementProperties
7 } from '../../../src/main/element-editor/shared'
8
9 describe('ensureElementAnchorInHtml', () => {
10 it('keeps an existing block id only when it is unique', () => {
11 const html = `
12 <html><body data-page-id="page">
13 <main data-block-id="content" data-role="content">
14 <p data-block-id="text">first</p>
15 <p data-block-id="text">second</p>
16 </main>
17 </body></html>
18 `
19
20 const result = ensureElementAnchorInHtml(html, {
21 pageId: 'page',
22 selector: 'body[data-page-id="page"] main > p:nth-child(2)',
23 elementTag: 'p'
24 })
25
26 expect(result.changed).toBe(true)
27 expect(result.blockId).not.toBe('text')
28 expect(result.selector).toBe(`body[data-page-id="page"] [data-block-id="${result.blockId}"]`)
29 expect(result.html).toContain(`<p data-block-id="${result.blockId}">second</p>`)
30 })
31
32 it('anchors rendered KaTeX itself instead of the surrounding formula block', () => {
33 const html = `
34 <html><body data-page-id="page">
35 <main data-block-id="content" data-role="content">
36 <div data-block-id="formula-card">
37 <span class="katex">
38 <span class="katex-mathml">
39 <math><semantics><mrow></mrow><annotation encoding="application/x-tex">x^2</annotation></semantics></math>
40 </span>
41 <span class="katex-html">x2</span>
42 </span>
43 </div>
44 </main>
45 </body></html>
46 `
47
48 const result = ensureElementAnchorInHtml(html, {
49 pageId: 'page',
50 selector: 'body[data-page-id="page"] .katex',
51 elementTag: 'span'
52 })
53
54 expect(result.selector).toBe(`body[data-page-id="page"] [data-block-id="${result.blockId}"]`)
55 expect(result.html).toContain(`<span class="katex" data-block-id="${result.blockId}">`)
56 expect(result.html).toContain('<div data-block-id="formula-card">')
57 })
58
59 it('materializes a runtime KaTeX formula when the source only has LaTeX delimiters', () => {
60 const html = `
61 <html><body data-page-id="page">
62 <main data-block-id="content" data-role="content">
63 <div data-block-id="formula-card">\\( a^2 + b^2 = c^2 \\)</div>
64 </main>
65 </body></html>
66 `
67
68 const result = ensureElementAnchorInHtml(html, {
69 pageId: 'page',
70 selector: 'body[data-page-id="page"] .ppt-page-root .katex',
71 elementTag: 'span',
72 formula: {
73 latex: 'a^2 + b^2 = c^2',
74 html: '<span class="katex"><span class="katex-mathml"><math><semantics><mrow></mrow><annotation encoding="application/x-tex">a^2 + b^2 = c^2</annotation></semantics></math></span><span class="katex-html">a2+b2=c2</span></span>',
75 displayMode: false
76 }
77 })
78
79 expect(result.selector).toBe(`body[data-page-id="page"] [data-block-id="${result.blockId}"]`)
80 expect(result.html).toContain(`<span class="katex" data-ppt-formula-latex="a^2 + b^2 = c^2"`)
81 expect(result.html).toContain(`data-block-id="${result.blockId}"`)
82 expect(result.html).not.toContain('\\( a^2 + b^2 = c^2 \\)')
83 })
84
85 })
86
87 describe('patchDraggedElementStyle chart sizing', () => {
88 it('does not persist canvas child width or height when resizing a chart frame', () => {
89 const html = `
90 <html><body data-page-id="page">
91 <div data-block-id="chart" class="ppt-chart-frame">
92 <canvas id="chart-canvas" class="h-full w-full"></canvas>
93 </div>
94 </body></html>
95 `
96
97 const result = patchDraggedElementStyle(
98 html,
99 'body[data-page-id="page"] [data-block-id="chart"]',
100 0,
101 0,
102 500,
103 320,
104 [{ path: [0], width: 500, height: 300 }],
105 false
106 )
107
108 expect(result).toContain('width: 500px; height: 320px')
109 expect(result).toContain('<canvas id="chart-canvas" class="h-full w-full"></canvas>')
110 expect(result).not.toContain('height: 300px')
111 })
112 })
113
114 describe('patchDraggedElementStyle flow layout resizing', () => {
115 const selector = 'body[data-page-id="page"] [data-block-id="flow"]'
116
117 it('persists translate and actual dimensions after layout isolation', () => {
118 const html = `
119 <html><body data-page-id="page">
120 <div data-block-id="flow" style="width: 240px; height: 100px">
121 <span data-block-id="child" style="width: 80px">Content</span>
122 </div>
123 </body></html>
124 `
125
126 const result = patchDraggedElementStyle(
127 html,
128 selector,
129 36,
130 -12,
131 360,
132 75,
133 [{ path: [0], width: 120, height: null }],
134 false
135 )
136
137 expect(result).toContain('--ppt-drag-x: 36px')
138 expect(result).toContain('--ppt-drag-y: -12px')
139 expect(result).toContain('width: 360px')
140 expect(result).toContain('height: 75px')
141 expect(result).toContain('width: 120px')
142 })
143 })
144
145 describe('patchGenericElementProperties rich text', () => {
146 it('updates inline rich text without flattening spans', () => {
147 const html = `
148 <html><body data-page-id="page">
149 <p data-block-id="text"><span style="color:#FB4526" data-block-id="text-1">南欧</span>旧文字</p>
150 </body></html>
151 `
152
153 const result = patchGenericElementProperties(
154 html,
155 'body[data-page-id="page"] [data-block-id="text"]',
156 {
157 html: '<span style="color:#FB4526" data-block-id="text-1">南欧</span>新文字'
158 }
159 )
160
161 expect(result).toContain(
162 '<span style="color:#FB4526" data-block-id="text-1">南欧</span>新文字'
163 )
164 })
165
166 it('strips editor-only zoom from rich text before writing html', () => {
167 const html = `
168 <html><body data-page-id="page">
169 <p data-block-id="text">旧文字</p>
170 </body></html>
171 `
172
173 const result = patchGenericElementProperties(
174 html,
175 'body[data-page-id="page"] [data-block-id="text"]',
176 {
177 html: '<span style="zoom: 0.5; color: #FB4526; font-size: 60px">新文字</span>'
178 }
179 )
180
181 expect(result).toContain('<span style="color: #FB4526; font-size: 60px">新文字</span>')
182 expect(result).not.toContain('zoom')
183 })
184
185 it('can update a bare text node by parent selector and child node index', () => {
186 const html = `
187 <html><body data-page-id="page">
188 <p data-block-id="text"><span data-block-id="text-1">南欧</span>旧文字</p>
189 </body></html>
190 `
191
192 const result = patchGenericElementProperties(
193 html,
194 'body[data-page-id="page"] [data-block-id="text"]',
195 {
196 text: '新文字',
197 textTarget: {
198 type: 'text-node',
199 parentSelector: 'body[data-page-id="page"] [data-block-id="text"]',
200 textNodeIndex: 1
201 }
202 }
203 )
204
205 expect(result).toContain('<span data-block-id="text-1">南欧</span>新文字')
206 })
207 })
208
209 describe('patchGenericElementProperties formula', () => {
210 const formulaHtml =
211 '<span class="katex"><span class="katex-mathml"><math><semantics><mrow></mrow><annotation encoding="application/x-tex">x^2</annotation></semantics></math></span><span class="katex-html">x2</span></span>'
212
213 it('updates an inline source formula without replacing surrounding text', () => {
214 const html = `
215 <html><body data-page-id="page">
216 <p data-block-id="text">函数 \\(x^2\\) 的导数</p>
217 </body></html>
218 `
219
220 const result = patchGenericElementProperties(
221 html,
222 'body[data-page-id="page"] [data-block-id="text"]',
223 {
224 formula: {
225 latex: 'x^3',
226 html: formulaHtml,
227 displayMode: false,
228 originalLatex: 'x^2'
229 }
230 }
231 )
232
233 expect(result).toContain('函数 \\(x^3\\) 的导数')
234 })
235
236 it('persists display mode changes by rewriting formula delimiters', () => {
237 const html = `
238 <html><body data-page-id="page">
239 <p data-block-id="text">函数 \\(x^2\\) 的导数</p>
240 </body></html>
241 `
242
243 const result = patchGenericElementProperties(
244 html,
245 'body[data-page-id="page"] [data-block-id="text"]',
246 {
247 formula: {
248 latex: 'x^2',
249 html: formulaHtml,
250 displayMode: true,
251 originalLatex: 'x^2'
252 }
253 }
254 )
255
256 expect(result).toContain('函数 \\[x^2\\] 的导数')
257 expect(result).not.toContain('\\(x^2\\)')
258 })
259
260 it('does not clear surrounding text when source formula matching fails', () => {
261 const html = `
262 <html><body data-page-id="page">
263 <p data-block-id="text">函数 \\(x^2\\) 与 \\(y^2\\) 的导数</p>
264 </body></html>
265 `
266
267 expect(() =>
268 patchGenericElementProperties(html, 'body[data-page-id="page"] [data-block-id="text"]', {
269 formula: {
270 latex: 'z^2',
271 html: formulaHtml,
272 displayMode: false,
273 originalLatex: 'missing'
274 }
275 })
276 ).toThrow('公式定位失败')
277 })
278
279 it('writes rendered KaTeX into an empty formula host', () => {
280 const html = `
281 <html><body data-page-id="page">
282 <div data-block-id="formula"></div>
283 </body></html>
284 `
285
286 const result = patchGenericElementProperties(
287 html,
288 'body[data-page-id="page"] [data-block-id="formula"]',
289 {
290 formula: {
291 latex: 'x^2',
292 html: formulaHtml,
293 displayMode: true,
294 originalLatex: ''
295 }
296 }
297 )
298
299 expect(result).toContain('class="katex"')
300 expect(result).toContain('data-ppt-formula-latex="x^2"')
301 expect(result).toContain('data-ppt-formula-display="true"')
302 })
303
304 it('strips unsafe markup from rendered formula html', () => {
305 const html = `
306 <html><body data-page-id="page">
307 <div data-block-id="formula"></div>
308 </body></html>
309 `
310
311 const result = patchGenericElementProperties(
312 html,
313 'body[data-page-id="page"] [data-block-id="formula"]',
314 {
315 formula: {
316 latex: 'x^2',
317 html: '<span class="katex" onclick="alert(1)"><script>alert(1)</script><span style="background:url(javascript:alert(1))">x2</span></span>',
318 displayMode: false,
319 originalLatex: ''
320 }
321 }
322 )
323
324 expect(result).toContain('class="katex"')
325 expect(result).not.toContain('<script')
326 expect(result).not.toContain('onclick')
327 expect(result).not.toContain('javascript:')
328 })
329
330 it('does not persist transient editor classes in formula html', () => {
331 const html = `
332 <html><body data-page-id="page">
333 <div data-block-id="formula"></div>
334 </body></html>
335 `
336
337 const result = patchGenericElementProperties(
338 html,
339 'body[data-page-id="page"] [data-block-id="formula"]',
340 {
341 formula: {
342 latex: 'x^2',
343 html: '<span class="katex arcsin1-presentation-editor-selected ppt-inspector-highlight"><span class="katex-html">x2</span></span>',
344 displayMode: false,
345 originalLatex: ''
346 }
347 }
348 )
349
350 expect(result).toContain('class="katex"')
351 expect(result).not.toContain('arcsin1-presentation-editor-selected')
352 expect(result).not.toContain('ppt-inspector-highlight')
353 })
354 })
355
356 describe('patchGenericElementProperties layer', () => {
357 it('persists negative z-index and makes static media positionable', () => {
358 const html = `
359 <html><body data-page-id="page">
360 <img data-block-id="media" src="./images/a.png" style="width: 100px">
361 </body></html>
362 `
363
364 const result = patchGenericElementProperties(
365 html,
366 'body[data-page-id="page"] [data-block-id="media"]',
367 {
368 style: { zIndex: -1 }
369 }
370 )
371
372 expect(result).toContain('width: 100px; position: relative; z-index: -1')
373 })
374 })
375
375 lines TYPESCRIPT