返回 oh-my-ppt
text-hit-selection.test.ts
根目录 / tests / unit / preview / text-hit-selection.test.ts
1 import path from 'path'
2 import fs from 'fs'
3 import { Window } from 'happy-dom'
4 import { describe, expect, it } from 'vitest'
5 import {
6 EDIT_MODE_CONSOLE_PREFIX,
7 buildEditModeInjectScript,
8 INSPECTOR_CONSOLE_PREFIX,
9 buildInspectorInjectScript
10 } from '@arcsin1/presentation-editor-runtime'
11
12 type Rect = Pick<DOMRect, 'left' | 'top' | 'right' | 'bottom' | 'width' | 'height'>
13
14 function setupInlineTextPage(script: string) {
15 const window = new Window({ url: 'http://localhost/page.html' }) as unknown as Window & {
16 ResizeObserver: typeof ResizeObserver
17 eval: (code: string) => void
18 }
19 const { document } = window
20 const logs: string[] = []
21
22 document.body.setAttribute('data-page-id', 'page')
23 document.body.innerHTML = `
24 <main class="ppt-page-root" data-ppt-guard-root="1">
25 <main data-block-id="content" data-role="content">
26 <p data-block-id="text">Alpha <span style="color:#FB4526" data-block-id="text-6">red text</span> omega</p>
27 </main>
28 </main>
29 `
30
31 const root = document.querySelector('.ppt-page-root') as HTMLElement
32 const content = document.querySelector('[data-block-id="content"]') as HTMLElement
33 const paragraph = document.querySelector('[data-block-id="text"]') as HTMLElement
34 const redSpan = document.querySelector('[data-block-id="text-6"]') as HTMLElement
35 const rects = new Map<Element, Rect>([
36 [root, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
37 [content, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
38 [paragraph, { left: 100, top: 100, right: 520, bottom: 130, width: 420, height: 30 }],
39 [redSpan, { left: 180, top: 100, right: 260, bottom: 130, width: 80, height: 30 }]
40 ])
41
42 window.HTMLElement.prototype.getBoundingClientRect = function () {
43 return rects.get(this) || { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
44 }
45 window.HTMLElement.prototype.getClientRects = function () {
46 return [this.getBoundingClientRect()]
47 }
48
49 const mockedDocument = document as Document & {
50 caretRangeFromPoint: () => { startContainer: ChildNode | null }
51 }
52 mockedDocument.elementFromPoint = () => paragraph
53 mockedDocument.elementsFromPoint = () => [paragraph, content, root]
54 mockedDocument.caretRangeFromPoint = () => ({ startContainer: redSpan.firstChild })
55 window.ResizeObserver = class {
56 observe() {}
57 disconnect() {}
58 } as unknown as typeof ResizeObserver
59 window.console.log = (value?: unknown) => logs.push(String(value))
60
61 window.eval(script)
62
63 return { window, paragraph, logs }
64 }
65
66 function setupOverlappingPage(script: string) {
67 const window = new Window({ url: 'http://localhost/page.html' }) as unknown as Window & {
68 ResizeObserver: typeof ResizeObserver
69 eval: (code: string) => void
70 }
71 const { document } = window
72 const logs: string[] = []
73
74 document.body.setAttribute('data-page-id', 'page')
75 document.body.innerHTML = `
76 <main class="ppt-page-root" data-ppt-guard-root="1">
77 <main data-block-id="content" data-role="content">
78 <div style="pointer-events:none" data-block-id="top">top visual layer</div>
79 <span data-block-id="behind">small behind layer</span>
80 </main>
81 </main>
82 `
83
84 const root = document.querySelector('.ppt-page-root') as HTMLElement
85 const content = document.querySelector('[data-block-id="content"]') as HTMLElement
86 const top = document.querySelector('[data-block-id="top"]') as HTMLElement
87 const behind = document.querySelector('[data-block-id="behind"]') as HTMLElement
88 const rects = new Map<Element, Rect>([
89 [root, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
90 [content, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
91 [top, { left: 100, top: 100, right: 420, bottom: 260, width: 320, height: 160 }],
92 [behind, { left: 150, top: 120, right: 180, bottom: 145, width: 30, height: 25 }]
93 ])
94
95 window.HTMLElement.prototype.getBoundingClientRect = function () {
96 return rects.get(this) || { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
97 }
98 window.HTMLElement.prototype.getClientRects = function () {
99 return [this.getBoundingClientRect()]
100 }
101
102 const mockedDocument = document as Document & {
103 caretRangeFromPoint: () => null
104 }
105 mockedDocument.elementFromPoint = () => behind
106 mockedDocument.elementsFromPoint = () =>
107 Array.from(document.head.querySelectorAll('style')).some((style) =>
108 style.textContent?.includes('pointer-events: auto')
109 )
110 ? [top, behind, content, root]
111 : [behind, content, root]
112 mockedDocument.caretRangeFromPoint = () => null
113 window.ResizeObserver = class {
114 observe() {}
115 disconnect() {}
116 } as unknown as typeof ResizeObserver
117 window.console.log = (value?: unknown) => logs.push(String(value))
118
119 window.eval(script)
120
121 return { window, eventTarget: behind, logs }
122 }
123
124 function setupMixedInlineParagraphPage(script: string) {
125 const window = new Window({ url: 'http://localhost/page.html' }) as unknown as Window & {
126 ResizeObserver: typeof ResizeObserver
127 eval: (code: string) => void
128 }
129 const { document } = window
130 const logs: string[] = []
131
132 document.body.setAttribute('data-page-id', 'page')
133 document.body.innerHTML = `
134 <main class="ppt-page-root" data-ppt-guard-root="1">
135 <main data-block-id="content" data-role="content">
136 <p data-block-id="text"><span data-block-id="text-1">南欧</span>(意、西、希)TFR在1.1-1.2区间徘徊超20年</p>
137 </main>
138 </main>
139 `
140
141 const root = document.querySelector('.ppt-page-root') as HTMLElement
142 const content = document.querySelector('[data-block-id="content"]') as HTMLElement
143 const paragraph = document.querySelector('[data-block-id="text"]') as HTMLElement
144 const span = document.querySelector('[data-block-id="text-1"]') as HTMLElement
145 const rects = new Map<Element, Rect>([
146 [root, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
147 [content, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
148 [paragraph, { left: 100, top: 100, right: 520, bottom: 130, width: 420, height: 30 }],
149 [span, { left: 100, top: 100, right: 140, bottom: 130, width: 40, height: 30 }]
150 ])
151
152 window.HTMLElement.prototype.getBoundingClientRect = function () {
153 return rects.get(this) || { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
154 }
155 window.HTMLElement.prototype.getClientRects = function () {
156 return [this.getBoundingClientRect()]
157 }
158
159 const mockedDocument = document as Document & {
160 caretRangeFromPoint: () => { startContainer: ChildNode | null }
161 }
162 mockedDocument.elementFromPoint = () => paragraph
163 mockedDocument.elementsFromPoint = () => [paragraph, content, root]
164 mockedDocument.caretRangeFromPoint = () => ({ startContainer: paragraph.lastChild })
165 window.ResizeObserver = class {
166 observe() {}
167 disconnect() {}
168 } as unknown as typeof ResizeObserver
169 window.console.log = (value?: unknown) => logs.push(String(value))
170
171 window.eval(script)
172
173 return { window, paragraph, logs }
174 }
175
176 function setupFormulaPage(script: string) {
177 const window = new Window({ url: 'http://localhost/page.html' }) as unknown as Window & {
178 ResizeObserver: typeof ResizeObserver
179 eval: (code: string) => void
180 }
181 const { document } = window
182 const logs: string[] = []
183
184 document.body.setAttribute('data-page-id', 'page')
185 document.body.innerHTML = `
186 <main class="ppt-page-root" data-ppt-guard-root="1">
187 <main data-block-id="content" data-role="content">
188 <div data-block-id="formula-card">
189 <span class="katex">
190 <span class="katex-mathml">
191 <math>
192 <semantics>
193 <mrow></mrow>
194 <annotation encoding="application/x-tex">x^2</annotation>
195 </semantics>
196 </math>
197 </span>
198 <span class="katex-html">x2</span>
199 </span>
200 </div>
201 </main>
202 </main>
203 `
204
205 const root = document.querySelector('.ppt-page-root') as HTMLElement
206 const content = document.querySelector('[data-block-id="content"]') as HTMLElement
207 const card = document.querySelector('[data-block-id="formula-card"]') as HTMLElement
208 const formula = document.querySelector('.katex') as HTMLElement
209 const formulaHtml = document.querySelector('.katex-html') as HTMLElement
210 const annotation = document.querySelector('annotation') as Element
211 const rects = new Map<Element, Rect>([
212 [root, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
213 [content, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
214 [card, { left: 100, top: 100, right: 520, bottom: 220, width: 420, height: 120 }],
215 [formula, { left: 160, top: 130, right: 300, bottom: 180, width: 140, height: 50 }],
216 [formulaHtml, { left: 160, top: 130, right: 300, bottom: 180, width: 140, height: 50 }],
217 [annotation, { left: 160, top: 130, right: 300, bottom: 180, width: 140, height: 50 }]
218 ])
219
220 window.HTMLElement.prototype.getBoundingClientRect = function () {
221 return rects.get(this) || { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
222 }
223 window.HTMLElement.prototype.getClientRects = function () {
224 return [this.getBoundingClientRect()]
225 }
226
227 const mockedDocument = document as Document & {
228 caretRangeFromPoint: () => null
229 }
230 mockedDocument.elementFromPoint = () => formulaHtml
231 mockedDocument.elementsFromPoint = () => [formulaHtml, formula, card, content, root]
232 mockedDocument.caretRangeFromPoint = () => null
233 window.ResizeObserver = class {
234 observe() {}
235 disconnect() {}
236 } as unknown as typeof ResizeObserver
237 window.console.log = (value?: unknown) => logs.push(String(value))
238
239 window.eval(script)
240
241 return { window, formula, formulaHtml, logs }
242 }
243
244 function setupAnchoredFormulaPage(script: string) {
245 const result = setupFormulaPage(script)
246 result.formula.setAttribute('data-block-id', 'formula-1')
247 return result
248 }
249
250 function setupFormulaWithoutAnnotationPage(script: string) {
251 const window = new Window({ url: 'http://localhost/page.html' }) as unknown as Window & {
252 ResizeObserver: typeof ResizeObserver
253 eval: (code: string) => void
254 }
255 const { document } = window
256 const logs: string[] = []
257
258 document.body.setAttribute('data-page-id', 'page')
259 document.body.innerHTML = `
260 <main class="ppt-page-root" data-ppt-guard-root="1">
261 <main data-block-id="content" data-role="content">
262 <p data-block-id="text">函数 <span class="katex"><span class="katex-html">x2</span></span> 的导数</p>
263 </main>
264 </main>
265 `
266
267 const root = document.querySelector('.ppt-page-root') as HTMLElement
268 const content = document.querySelector('[data-block-id="content"]') as HTMLElement
269 const paragraph = document.querySelector('[data-block-id="text"]') as HTMLElement
270 const formula = document.querySelector('.katex') as HTMLElement
271 const formulaHtml = document.querySelector('.katex-html') as HTMLElement
272 const rects = new Map<Element, Rect>([
273 [root, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
274 [content, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
275 [paragraph, { left: 100, top: 100, right: 520, bottom: 130, width: 420, height: 30 }],
276 [formula, { left: 160, top: 100, right: 220, bottom: 130, width: 60, height: 30 }],
277 [formulaHtml, { left: 160, top: 100, right: 220, bottom: 130, width: 60, height: 30 }]
278 ])
279
280 window.HTMLElement.prototype.getBoundingClientRect = function () {
281 return rects.get(this) || { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
282 }
283 window.HTMLElement.prototype.getClientRects = function () {
284 return [this.getBoundingClientRect()]
285 }
286
287 const mockedDocument = document as Document & {
288 caretRangeFromPoint: () => null
289 }
290 mockedDocument.elementFromPoint = () => formulaHtml
291 mockedDocument.elementsFromPoint = () => [formulaHtml, formula, paragraph, content, root]
292 mockedDocument.caretRangeFromPoint = () => null
293 window.ResizeObserver = class {
294 observe() {}
295 disconnect() {}
296 } as unknown as typeof ResizeObserver
297 window.console.log = (value?: unknown) => logs.push(String(value))
298
299 window.eval(script)
300
301 return { window, formulaHtml, logs }
302 }
303
304 function setupDisplayFormulaPage(script: string) {
305 const window = new Window({ url: 'http://localhost/page.html' }) as unknown as Window & {
306 ResizeObserver: typeof ResizeObserver
307 eval: (code: string) => void
308 }
309 const { document } = window
310 const logs: string[] = []
311
312 document.body.setAttribute('data-page-id', 'page')
313 document.body.innerHTML = `
314 <main class="ppt-page-root" data-ppt-guard-root="1">
315 <main data-block-id="content" data-role="content">
316 <div data-block-id="formula-card">
317 <span class="katex-display">
318 <span class="katex">
319 <span class="katex-mathml">
320 <math>
321 <semantics>
322 <mrow></mrow>
323 <annotation encoding="application/x-tex">x^2</annotation>
324 </semantics>
325 </math>
326 </span>
327 <span class="katex-html">
328 <span class="base">x2</span>
329 </span>
330 </span>
331 </span>
332 </div>
333 </main>
334 </main>
335 `
336
337 const root = document.querySelector('.ppt-page-root') as HTMLElement
338 const content = document.querySelector('[data-block-id="content"]') as HTMLElement
339 const card = document.querySelector('[data-block-id="formula-card"]') as HTMLElement
340 const display = document.querySelector('.katex-display') as HTMLElement
341 const formula = document.querySelector('.katex') as HTMLElement
342 const formulaHtml = document.querySelector('.katex-html') as HTMLElement
343 const base = document.querySelector('.base') as HTMLElement
344 const rects = new Map<Element, Rect>([
345 [root, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
346 [content, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
347 [card, { left: 100, top: 100, right: 560, bottom: 260, width: 460, height: 160 }],
348 [display, { left: 120, top: 110, right: 520, bottom: 230, width: 400, height: 120 }],
349 [formula, { left: 120, top: 110, right: 520, bottom: 230, width: 400, height: 120 }],
350 [formulaHtml, { left: 210, top: 145, right: 330, bottom: 185, width: 120, height: 40 }],
351 [base, { left: 210, top: 145, right: 330, bottom: 185, width: 120, height: 40 }]
352 ])
353
354 window.HTMLElement.prototype.getBoundingClientRect = function () {
355 return rects.get(this) || { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
356 }
357 window.HTMLElement.prototype.getClientRects = function () {
358 return [this.getBoundingClientRect()]
359 }
360
361 const mockedDocument = document as Document & {
362 caretRangeFromPoint: () => null
363 }
364 mockedDocument.elementFromPoint = () => display
365 mockedDocument.elementsFromPoint = () => [display, card, content, root]
366 mockedDocument.caretRangeFromPoint = () => null
367 window.ResizeObserver = class {
368 observe() {}
369 disconnect() {}
370 } as unknown as typeof ResizeObserver
371 window.console.log = (value?: unknown) => logs.push(String(value))
372
373 window.eval(script)
374
375 return { window, display, formula, formulaHtml, logs }
376 }
377
378 function setupFormulaHostHitPage(script: string) {
379 const window = new Window({ url: 'http://localhost/page.html' }) as unknown as Window & {
380 ResizeObserver: typeof ResizeObserver
381 eval: (code: string) => void
382 }
383 const { document } = window
384 const logs: string[] = []
385
386 document.body.setAttribute('data-page-id', 'page')
387 document.body.innerHTML = `
388 <main class="ppt-page-root" data-ppt-guard-root="1">
389 <main data-block-id="content" data-role="content">
390 <div data-block-id="formula-card">
391 <span data-block-id="formula-label">GAUSSIAN INTEGRAL</span>
392 <span class="katex" data-ppt-formula-latex="\\int_0^\\infty e^{-x^2}\\,dx = \\frac{\\sqrt{\\pi}}{2}">
393 <span class="katex-mathml">
394 <math>
395 <semantics>
396 <mrow></mrow>
397 <annotation encoding="application/x-tex">\\int_0^\\infty e^{-x^2}\\,dx = \\frac{\\sqrt{\\pi}}{2}</annotation>
398 </semantics>
399 </math>
400 </span>
401 <span class="katex-html">
402 <span class="base">integral glyphs</span>
403 </span>
404 </span>
405 </div>
406 </main>
407 </main>
408 `
409
410 const root = document.querySelector('.ppt-page-root') as HTMLElement
411 const content = document.querySelector('[data-block-id="content"]') as HTMLElement
412 const card = document.querySelector('[data-block-id="formula-card"]') as HTMLElement
413 const label = document.querySelector('[data-block-id="formula-label"]') as HTMLElement
414 const formula = document.querySelector('.katex') as HTMLElement
415 const formulaHtml = document.querySelector('.katex-html') as HTMLElement
416 const base = document.querySelector('.base') as HTMLElement
417 const rects = new Map<Element, Rect>([
418 [root, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
419 [content, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
420 [card, { left: 80, top: 70, right: 750, bottom: 220, width: 670, height: 150 }],
421 [label, { left: 82, top: 92, right: 748, bottom: 112, width: 666, height: 20 }],
422 [formula, { left: 280, top: 140, right: 540, bottom: 195, width: 260, height: 55 }],
423 [formulaHtml, { left: 280, top: 140, right: 540, bottom: 195, width: 260, height: 55 }],
424 [base, { left: 280, top: 140, right: 540, bottom: 195, width: 260, height: 55 }]
425 ])
426
427 window.HTMLElement.prototype.getBoundingClientRect = function () {
428 return rects.get(this) || { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
429 }
430 window.HTMLElement.prototype.getClientRects = function () {
431 return [this.getBoundingClientRect()]
432 }
433
434 const mockedDocument = document as Document & {
435 caretRangeFromPoint: () => null
436 }
437 mockedDocument.elementFromPoint = () => card
438 mockedDocument.elementsFromPoint = () => [card, content, root]
439 mockedDocument.caretRangeFromPoint = () => null
440 window.ResizeObserver = class {
441 observe() {}
442 disconnect() {}
443 } as unknown as typeof ResizeObserver
444 window.console.log = (value?: unknown) => logs.push(String(value))
445
446 window.eval(script)
447
448 return { window, card, formula, logs }
449 }
450
451 function setupChartPage(script: string) {
452 const window = new Window({ url: 'http://localhost/page.html' }) as unknown as Window & {
453 ResizeObserver: typeof ResizeObserver
454 eval: (code: string) => void
455 }
456 const { document } = window
457 const logs: string[] = []
458
459 document.body.setAttribute('data-page-id', 'page')
460 document.body.innerHTML = `
461 <main class="ppt-page-root" data-ppt-guard-root="1">
462 <main data-block-id="content" data-role="content">
463 <div data-block-id="chart-frame" class="ppt-chart-frame">
464 <canvas id="chart-canvas"></canvas>
465 </div>
466 </main>
467 </main>
468 `
469
470 const root = document.querySelector('.ppt-page-root') as HTMLElement
471 const content = document.querySelector('[data-block-id="content"]') as HTMLElement
472 const frame = document.querySelector('[data-block-id="chart-frame"]') as HTMLElement
473 const canvas = document.querySelector('#chart-canvas') as HTMLElement
474 const rects = new Map<Element, Rect>([
475 [root, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
476 [content, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
477 [frame, { left: 120, top: 120, right: 420, bottom: 320, width: 300, height: 200 }],
478 [canvas, { left: 120, top: 120, right: 420, bottom: 320, width: 300, height: 200 }]
479 ])
480
481 window.HTMLElement.prototype.getBoundingClientRect = function () {
482 return rects.get(this) || { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
483 }
484 window.HTMLElement.prototype.getClientRects = function () {
485 return [this.getBoundingClientRect()]
486 }
487
488 const mockedDocument = document as Document & {
489 caretRangeFromPoint: () => null
490 }
491 mockedDocument.elementFromPoint = () => canvas
492 mockedDocument.elementsFromPoint = () => [canvas, frame, content, root]
493 mockedDocument.caretRangeFromPoint = () => null
494 window.ResizeObserver = class {
495 observe() {}
496 disconnect() {}
497 } as unknown as typeof ResizeObserver
498 window.console.log = (value?: unknown) => logs.push(String(value))
499
500 window.eval(script)
501
502 return { window, frame, canvas, logs }
503 }
504
505 function setupSvgPage(script: string) {
506 const window = new Window({ url: 'http://localhost/page.html' }) as unknown as Window & {
507 ResizeObserver: typeof ResizeObserver
508 eval: (code: string) => void
509 }
510 const { document } = window
511 const logs: string[] = []
512
513 document.body.setAttribute('data-page-id', 'page')
514 document.body.innerHTML = `
515 <main class="ppt-page-root" data-ppt-guard-root="1">
516 <main data-block-id="content" data-role="content">
517 <div data-block-id="logo" class="logo-host">
518 <svg viewBox="0 0 100 100">
519 <path d="M10 10 L90 10 L50 90 Z"></path>
520 </svg>
521 </div>
522 </main>
523 </main>
524 `
525
526 const root = document.querySelector('.ppt-page-root') as HTMLElement
527 const content = document.querySelector('[data-block-id="content"]') as HTMLElement
528 const logo = document.querySelector('[data-block-id="logo"]') as HTMLElement
529 const svg = document.querySelector('svg') as HTMLElement
530 const path = document.querySelector('path') as Element
531 const rects = new Map<Element, Rect>([
532 [root, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
533 [content, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
534 [logo, { left: 180, top: 110, right: 340, bottom: 270, width: 160, height: 160 }],
535 [svg, { left: 180, top: 110, right: 340, bottom: 270, width: 160, height: 160 }],
536 [path, { left: 190, top: 120, right: 330, bottom: 260, width: 140, height: 140 }]
537 ])
538
539 window.HTMLElement.prototype.getBoundingClientRect = function () {
540 return rects.get(this) || { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
541 }
542 window.SVGElement.prototype.getBoundingClientRect = function () {
543 return rects.get(this) || { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
544 }
545 window.HTMLElement.prototype.getClientRects = function () {
546 return [this.getBoundingClientRect()]
547 }
548 window.SVGElement.prototype.getClientRects = function () {
549 return [this.getBoundingClientRect()]
550 }
551
552 const mockedDocument = document as Document & {
553 caretRangeFromPoint: () => null
554 }
555 mockedDocument.elementFromPoint = () => path
556 mockedDocument.elementsFromPoint = () => [path as unknown as Element, svg, logo, content, root]
557 mockedDocument.caretRangeFromPoint = () => null
558 window.ResizeObserver = class {
559 observe() {}
560 disconnect() {}
561 } as unknown as typeof ResizeObserver
562 window.console.log = (value?: unknown) => logs.push(String(value))
563
564 window.eval(script)
565
566 return { window, logo, path, logs }
567 }
568
569 function setupGeneratedBackgroundPage(script: string) {
570 const window = new Window({ url: 'http://localhost/page.html' }) as unknown as Window & {
571 ResizeObserver: typeof ResizeObserver
572 eval: (code: string) => void
573 }
574 const { document } = window
575 const logs: string[] = []
576
577 document.body.setAttribute('data-page-id', 'page')
578 document.body.innerHTML = `
579 <main class="ppt-page-root" data-ppt-guard-root="1">
580 <main data-block-id="content" data-role="content">
581 <div data-block-id="hero">Hero content</div>
582 <img data-block-id="bg" data-ppt-generated-background="1" src="./bg.png" alt="">
583 </main>
584 </main>
585 `
586
587 const root = document.querySelector('.ppt-page-root') as HTMLElement
588 const content = document.querySelector('[data-block-id="content"]') as HTMLElement
589 const hero = document.querySelector('[data-block-id="hero"]') as HTMLElement
590 const background = document.querySelector('[data-ppt-generated-background="1"]') as HTMLElement
591 const rects = new Map<Element, Rect>([
592 [root, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
593 [content, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
594 [hero, { left: 120, top: 120, right: 520, bottom: 280, width: 400, height: 160 }],
595 [background, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }]
596 ])
597
598 window.HTMLElement.prototype.getBoundingClientRect = function () {
599 return rects.get(this) || { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
600 }
601 window.HTMLElement.prototype.getClientRects = function () {
602 return [this.getBoundingClientRect()]
603 }
604
605 const mockedDocument = document as Document & {
606 caretRangeFromPoint: () => null
607 }
608 mockedDocument.elementFromPoint = () =>
609 background.style.visibility === 'hidden' ? hero : background
610 mockedDocument.elementsFromPoint = () =>
611 background.style.visibility === 'hidden'
612 ? [hero, content, root]
613 : Array.from(document.head.querySelectorAll('style')).some((style) =>
614 style.textContent?.includes('pointer-events: auto')
615 )
616 ? [background, hero, content, root]
617 : [background, content, root]
618 mockedDocument.caretRangeFromPoint = () => null
619 window.ResizeObserver = class {
620 observe() {}
621 disconnect() {}
622 } as unknown as typeof ResizeObserver
623 window.console.log = (value?: unknown) => logs.push(String(value))
624
625 window.eval(script)
626
627 return { window, hero, background, logs }
628 }
629
630 function setupOverflowBoxPage(script: string) {
631 const window = new Window({ url: 'http://localhost/page.html' }) as unknown as Window & {
632 ResizeObserver: typeof ResizeObserver
633 eval: (code: string) => void
634 }
635 const { document } = window
636 const logs: string[] = []
637
638 document.body.setAttribute('data-page-id', 'page')
639 document.body.innerHTML = `
640 <main class="ppt-page-root" data-ppt-guard-root="1">
641 <main data-block-id="content" data-role="content">
642 <div data-block-id="card">
643 <span class="overflow-child">Overflowing child</span>
644 </div>
645 </main>
646 </main>
647 `
648
649 const root = document.querySelector('.ppt-page-root') as HTMLElement
650 const content = document.querySelector('[data-block-id="content"]') as HTMLElement
651 const card = document.querySelector('[data-block-id="card"]') as HTMLElement
652 const child = document.querySelector('.overflow-child') as HTMLElement
653 const rects = new Map<Element, Rect>([
654 [root, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
655 [content, { left: 0, top: 0, right: 1000, bottom: 600, width: 1000, height: 600 }],
656 [card, { left: 180, top: 120, right: 280, bottom: 160, width: 100, height: 40 }],
657 [child, { left: 170, top: 110, right: 360, bottom: 210, width: 190, height: 100 }]
658 ])
659
660 window.HTMLElement.prototype.getBoundingClientRect = function () {
661 return rects.get(this) || { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
662 }
663 window.HTMLElement.prototype.getClientRects = function () {
664 return [this.getBoundingClientRect()]
665 }
666
667 const mockedDocument = document as Document & {
668 caretRangeFromPoint: () => null
669 }
670 mockedDocument.elementFromPoint = () => card
671 mockedDocument.elementsFromPoint = () => [card, child, content, root]
672 mockedDocument.caretRangeFromPoint = () => null
673 window.ResizeObserver = class {
674 observe() {}
675 disconnect() {}
676 } as unknown as typeof ResizeObserver
677 window.console.log = (value?: unknown) => logs.push(String(value))
678
679 window.eval(script)
680
681 return { window, card, logs }
682 }
683
684 function readPayload(logs: string[], prefix: string) {
685 const log = logs.findLast((item) => item.startsWith(prefix))
686 expect(log).toBeTruthy()
687 return JSON.parse(String(log).slice(prefix.length)) as { selector?: string; elementTag?: string }
688 }
689
690 describe('preview text hit selection', () => {
691 it('animation-select inspector mode does not freeze motion', () => {
692 const script = buildInspectorInjectScript({ mode: 'animation-select' })
693 expect(script).toContain('MODE = "animation-select"')
694 expect(script).toContain('window.__pptInspectorRestoreSelection')
695 expect(script).toContain('shouldFreezeMotion')
696 })
697
698 it('preview iframe does not install a click animation bridge', async () => {
699 const source = fs.readFileSync(
700 path.resolve(__dirname, '../../../src/renderer/src/components/preview/PreviewIframe.tsx'),
701 'utf-8'
702 )
703 expect(source).not.toContain('preview-click-animation')
704 expect(source).toContain("url.searchParams.set('pptPlayback', '0')")
705 expect(source).toContain("url.searchParams.set('print', '1')")
706 })
707
708 it('inspector selects the inline span under the text caret point', () => {
709 const { window, paragraph, logs } = setupInlineTextPage(buildInspectorInjectScript())
710
711 paragraph.dispatchEvent(
712 new window.MouseEvent('click', {
713 bubbles: true,
714 cancelable: true,
715 clientX: 190,
716 clientY: 115
717 })
718 )
719
720 const payload = readPayload(logs, INSPECTOR_CONSOLE_PREFIX)
721 expect(payload.elementTag).toBe('span')
722 expect(payload.selector).toBe('body[data-page-id="page"] [data-block-id="text-6"]')
723 })
724
725 it('edit mode selects the inline span under the text caret point', () => {
726 const { window, paragraph, logs } = setupInlineTextPage(buildEditModeInjectScript())
727
728 paragraph.dispatchEvent(
729 new window.PointerEvent('pointerdown', {
730 bubbles: true,
731 cancelable: true,
732 button: 0,
733 clientX: 190,
734 clientY: 115,
735 pointerId: 1
736 })
737 )
738 paragraph.dispatchEvent(
739 new window.PointerEvent('pointerup', {
740 bubbles: true,
741 cancelable: true,
742 button: 0,
743 clientX: 190,
744 clientY: 115,
745 pointerId: 1
746 })
747 )
748
749 const payload = readPayload(logs, EDIT_MODE_CONSOLE_PREFIX)
750 expect(payload.elementTag).toBe('span')
751 expect(payload.selector).toBe('body[data-page-id="page"] [data-block-id="text-6"]')
752 })
753
754 it('edit mode does not treat a long pointer release as a selection click', () => {
755 const { window, paragraph, logs } = setupInlineTextPage(buildEditModeInjectScript())
756
757 paragraph.dispatchEvent(
758 new window.PointerEvent('pointerdown', {
759 bubbles: true,
760 cancelable: true,
761 button: 0,
762 clientX: 190,
763 clientY: 115,
764 pointerId: 1
765 })
766 )
767 paragraph.dispatchEvent(
768 new window.PointerEvent('pointerup', {
769 bubbles: true,
770 cancelable: true,
771 button: 0,
772 clientX: 240,
773 clientY: 115,
774 pointerId: 1
775 })
776 )
777
778 expect(logs.some((item) => item.startsWith(EDIT_MODE_CONSOLE_PREFIX))).toBe(false)
779 })
780
781 it('inspector follows the browser hit-test stack before geometry size', () => {
782 const { window, eventTarget, logs } = setupOverlappingPage(buildInspectorInjectScript())
783
784 eventTarget.dispatchEvent(
785 new window.MouseEvent('click', {
786 bubbles: true,
787 cancelable: true,
788 clientX: 160,
789 clientY: 130
790 })
791 )
792
793 const payload = readPayload(logs, INSPECTOR_CONSOLE_PREFIX)
794 expect(payload.selector).toBe('body[data-page-id="page"] [data-block-id="top"]')
795 })
796
797 it('edit mode follows the browser hit-test stack before geometry size', () => {
798 const { window, eventTarget, logs } = setupOverlappingPage(buildEditModeInjectScript())
799
800 eventTarget.dispatchEvent(
801 new window.PointerEvent('pointerdown', {
802 bubbles: true,
803 cancelable: true,
804 button: 0,
805 clientX: 160,
806 clientY: 130,
807 pointerId: 1
808 })
809 )
810 eventTarget.dispatchEvent(
811 new window.PointerEvent('pointerup', {
812 bubbles: true,
813 cancelable: true,
814 button: 0,
815 clientX: 160,
816 clientY: 130,
817 pointerId: 1
818 })
819 )
820
821 const payload = readPayload(logs, EDIT_MODE_CONSOLE_PREFIX)
822 expect(payload.selector).toBe('body[data-page-id="page"] [data-block-id="top"]')
823 })
824
825 it('edit mode treats mixed inline paragraphs as editable text', () => {
826 const { window, paragraph, logs } = setupMixedInlineParagraphPage(buildEditModeInjectScript())
827
828 paragraph.dispatchEvent(
829 new window.PointerEvent('pointerdown', {
830 bubbles: true,
831 cancelable: true,
832 button: 0,
833 clientX: 180,
834 clientY: 115,
835 pointerId: 1
836 })
837 )
838 paragraph.dispatchEvent(
839 new window.PointerEvent('pointerup', {
840 bubbles: true,
841 cancelable: true,
842 button: 0,
843 clientX: 180,
844 clientY: 115,
845 pointerId: 1
846 })
847 )
848
849 const payload = readPayload(logs, EDIT_MODE_CONSOLE_PREFIX) as {
850 isText?: boolean
851 selector?: string
852 text?: string
853 html?: string
854 textTarget?: {
855 parentSelector?: string
856 textNodeIndex?: number
857 text?: string
858 }
859 }
860 expect(payload.selector).toBe('body[data-page-id="page"] [data-block-id="text"]')
861 expect(payload.isText).toBe(true)
862 expect(payload.text).toBe('南欧(意、西、希)TFR在1.1-1.2区间徘徊超20年')
863 expect(payload.html).toBe(
864 '<span data-block-id="text-1">南欧</span>(意、西、希)TFR在1.1-1.2区间徘徊超20年'
865 )
866 expect(payload.textTarget).toMatchObject({
867 parentSelector: 'body[data-page-id="page"] [data-block-id="text"]',
868 textNodeIndex: 1,
869 text: '(意、西、希)TFR在1.1-1.2区间徘徊超20年'
870 })
871 })
872
873 it('inspector selects the KaTeX node instead of its formula container', () => {
874 const { window, formula, formulaHtml, logs } = setupFormulaPage(buildInspectorInjectScript())
875
876 formulaHtml.dispatchEvent(
877 new window.MouseEvent('click', {
878 bubbles: true,
879 cancelable: true,
880 clientX: 180,
881 clientY: 145
882 })
883 )
884
885 const payload = readPayload(logs, INSPECTOR_CONSOLE_PREFIX)
886 expect(payload.elementTag).toBe('span')
887 expect(payload.selector).not.toContain('formula-card')
888 expect(window.document.querySelector(payload.selector || '')).toBe(formula)
889 })
890
891 it('animation-select includes formula metadata for runtime-rendered formulas', () => {
892 const { window, formulaHtml, logs } = setupFormulaPage(
893 buildInspectorInjectScript({ mode: 'animation-select' })
894 )
895
896 formulaHtml.dispatchEvent(
897 new window.MouseEvent('click', {
898 bubbles: true,
899 cancelable: true,
900 clientX: 180,
901 clientY: 145
902 })
903 )
904
905 const payload = readPayload(logs, INSPECTOR_CONSOLE_PREFIX) as {
906 mode?: string
907 formula?: { latex?: string; html?: string; displayMode?: boolean }
908 }
909 expect(payload.mode).toBe('animation-select')
910 expect(payload.formula).toMatchObject({
911 latex: 'x^2',
912 displayMode: false
913 })
914 expect(payload.formula?.html).toContain('class="katex"')
915 })
916
917 it('inspector selects the formula when hit testing returns the formula host', () => {
918 const { window, card, formula, logs } = setupFormulaHostHitPage(buildInspectorInjectScript())
919
920 card.dispatchEvent(
921 new window.MouseEvent('mousemove', {
922 bubbles: true,
923 cancelable: true,
924 clientX: 360,
925 clientY: 166
926 })
927 )
928 card.dispatchEvent(
929 new window.MouseEvent('click', {
930 bubbles: true,
931 cancelable: true,
932 clientX: 360,
933 clientY: 166
934 })
935 )
936
937 const payload = readPayload(logs, INSPECTOR_CONSOLE_PREFIX)
938 expect(payload.selector).not.toContain('formula-card')
939 expect(window.document.querySelector(payload.selector || '')).toBe(formula)
940 const overlay = window.document.getElementById('ppt-inspector-highlight-overlay') as HTMLElement
941 expect(overlay.style.left).toBe('276px')
942 expect(overlay.style.top).toBe('136px')
943 expect(overlay.style.width).toBe('268px')
944 expect(overlay.style.height).toBe('63px')
945 })
946
947 it('inspector does not steal the formula host when the point is outside formula glyphs', () => {
948 const { window, card, formula, logs } = setupFormulaHostHitPage(buildInspectorInjectScript())
949
950 card.dispatchEvent(
951 new window.MouseEvent('click', {
952 bubbles: true,
953 cancelable: true,
954 clientX: 360,
955 clientY: 102
956 })
957 )
958
959 const payload = readPayload(logs, INSPECTOR_CONSOLE_PREFIX)
960 expect(window.document.querySelector(payload.selector || '')).toBe(card)
961 expect(window.document.querySelector(payload.selector || '')).not.toBe(formula)
962 })
963
964 it('inspector selects the chart frame when clicking the canvas', () => {
965 const { window, frame, canvas, logs } = setupChartPage(buildInspectorInjectScript())
966
967 canvas.dispatchEvent(
968 new window.MouseEvent('click', {
969 bubbles: true,
970 cancelable: true,
971 clientX: 200,
972 clientY: 180
973 })
974 )
975
976 const payload = readPayload(logs, INSPECTOR_CONSOLE_PREFIX)
977 expect(payload.elementTag).toBe('div')
978 expect(payload.selector).toBe('body[data-page-id="page"] [data-block-id="chart-frame"]')
979 expect(window.document.querySelector(payload.selector || '')).toBe(frame)
980 })
981
982 it('inspector keeps generated backgrounds from stealing selection', () => {
983 const { window, hero, background, logs } = setupGeneratedBackgroundPage(
984 buildInspectorInjectScript()
985 )
986
987 background.dispatchEvent(
988 new window.MouseEvent('click', {
989 bubbles: true,
990 cancelable: true,
991 clientX: 220,
992 clientY: 180
993 })
994 )
995
996 const payload = readPayload(logs, INSPECTOR_CONSOLE_PREFIX)
997 expect(payload.selector).toBe('body[data-page-id="page"] [data-block-id="hero"]')
998 expect(window.document.querySelector(payload.selector || '')).toBe(hero)
999 })
1000
1001 it('inspector selects the svg owner instead of the nested svg path', () => {
1002 const { window, logo, path, logs } = setupSvgPage(buildInspectorInjectScript())
1003
1004 path.dispatchEvent(
1005 new window.MouseEvent('click', {
1006 bubbles: true,
1007 cancelable: true,
1008 clientX: 240,
1009 clientY: 180
1010 })
1011 )
1012
1013 const payload = readPayload(logs, INSPECTOR_CONSOLE_PREFIX)
1014 expect(payload.elementTag).toBe('div')
1015 expect(payload.selector).toBe('body[data-page-id="page"] [data-block-id="logo"]')
1016 expect(window.document.querySelector(payload.selector || '')).toBe(logo)
1017 })
1018
1019 it('edit mode selects the KaTeX node instead of its formula container', () => {
1020 const { window, formula, formulaHtml, logs } = setupFormulaPage(buildEditModeInjectScript())
1021
1022 formulaHtml.dispatchEvent(
1023 new window.PointerEvent('pointerdown', {
1024 bubbles: true,
1025 cancelable: true,
1026 button: 0,
1027 clientX: 180,
1028 clientY: 145,
1029 pointerId: 1
1030 })
1031 )
1032 formulaHtml.dispatchEvent(
1033 new window.PointerEvent('pointerup', {
1034 bubbles: true,
1035 cancelable: true,
1036 button: 0,
1037 clientX: 180,
1038 clientY: 145,
1039 pointerId: 1
1040 })
1041 )
1042
1043 const payload = readPayload(logs, EDIT_MODE_CONSOLE_PREFIX)
1044 expect(payload.elementTag).toBe('span')
1045 expect(payload.selector).not.toContain('formula-card')
1046 expect(window.document.querySelector(payload.selector || '')).toBe(formula)
1047 expect(payload.kind).toBe('formula')
1048 expect(payload.capabilities).toContain('formula')
1049 expect(payload.snapshot?.formula?.html).toContain('class="katex"')
1050 expect(payload.snapshot?.formula?.html).not.toContain('arcsin1-presentation-editor-selected')
1051 })
1052
1053 it('edit mode selects the formula when hit testing returns the formula host', () => {
1054 const { window, card, formula, logs } = setupFormulaHostHitPage(buildEditModeInjectScript())
1055
1056 card.dispatchEvent(
1057 new window.PointerEvent('pointerdown', {
1058 bubbles: true,
1059 cancelable: true,
1060 button: 0,
1061 clientX: 360,
1062 clientY: 166,
1063 pointerId: 1
1064 })
1065 )
1066 card.dispatchEvent(
1067 new window.PointerEvent('pointerup', {
1068 bubbles: true,
1069 cancelable: true,
1070 button: 0,
1071 clientX: 360,
1072 clientY: 166,
1073 pointerId: 1
1074 })
1075 )
1076
1077 const payload = readPayload(logs, EDIT_MODE_CONSOLE_PREFIX)
1078 expect(payload.selector).not.toContain('formula-card')
1079 expect(window.document.querySelector(payload.selector || '')).toBe(formula)
1080 expect(payload.kind).toBe('formula')
1081 const overlay = window.document.getElementById('arcsin1-presentation-editor-resize-overlay') as HTMLElement
1082 expect(overlay.style.left).toBe('280px')
1083 expect(overlay.style.top).toBe('140px')
1084 expect(overlay.style.width).toBe('260px')
1085 expect(overlay.style.height).toBe('55px')
1086 })
1087
1088 it('edit mode does not steal the formula host when the point is outside formula glyphs', () => {
1089 const { window, card, formula, logs } = setupFormulaHostHitPage(buildEditModeInjectScript())
1090
1091 card.dispatchEvent(
1092 new window.PointerEvent('pointerdown', {
1093 bubbles: true,
1094 cancelable: true,
1095 button: 0,
1096 clientX: 360,
1097 clientY: 102,
1098 pointerId: 1
1099 })
1100 )
1101 card.dispatchEvent(
1102 new window.PointerEvent('pointerup', {
1103 bubbles: true,
1104 cancelable: true,
1105 button: 0,
1106 clientX: 360,
1107 clientY: 102,
1108 pointerId: 1
1109 })
1110 )
1111
1112 const payload = readPayload(logs, EDIT_MODE_CONSOLE_PREFIX)
1113 expect(window.document.querySelector(payload.selector || '')).toBe(card)
1114 expect(window.document.querySelector(payload.selector || '')).not.toBe(formula)
1115 })
1116
1117 it('edit mode selects the chart frame when clicking the canvas', () => {
1118 const { window, frame, canvas, logs } = setupChartPage(buildEditModeInjectScript())
1119
1120 canvas.dispatchEvent(
1121 new window.PointerEvent('pointerdown', {
1122 bubbles: true,
1123 cancelable: true,
1124 button: 0,
1125 clientX: 200,
1126 clientY: 180,
1127 pointerId: 1
1128 })
1129 )
1130 canvas.dispatchEvent(
1131 new window.PointerEvent('pointerup', {
1132 bubbles: true,
1133 cancelable: true,
1134 button: 0,
1135 clientX: 200,
1136 clientY: 180,
1137 pointerId: 1
1138 })
1139 )
1140
1141 const payload = readPayload(logs, EDIT_MODE_CONSOLE_PREFIX)
1142 expect(payload.elementTag).toBe('div')
1143 expect(payload.selector).toBe('body[data-page-id="page"] [data-block-id="chart-frame"]')
1144 expect(window.document.querySelector(payload.selector || '')).toBe(frame)
1145 })
1146
1147 it('edit mode selects the svg owner instead of the nested svg path', () => {
1148 const { window, logo, path, logs } = setupSvgPage(buildEditModeInjectScript())
1149
1150 path.dispatchEvent(
1151 new window.PointerEvent('pointerdown', {
1152 bubbles: true,
1153 cancelable: true,
1154 button: 0,
1155 clientX: 240,
1156 clientY: 180,
1157 pointerId: 1
1158 })
1159 )
1160 path.dispatchEvent(
1161 new window.PointerEvent('pointerup', {
1162 bubbles: true,
1163 cancelable: true,
1164 button: 0,
1165 clientX: 240,
1166 clientY: 180,
1167 pointerId: 1
1168 })
1169 )
1170
1171 const payload = readPayload(logs, EDIT_MODE_CONSOLE_PREFIX)
1172 expect(payload.elementTag).toBe('div')
1173 expect(payload.selector).toBe('body[data-page-id="page"] [data-block-id="logo"]')
1174 expect(window.document.querySelector(payload.selector || '')).toBe(logo)
1175 })
1176
1177 it('keeps the formula block id after live formula replacement', () => {
1178 const { window, formulaHtml, logs } = setupAnchoredFormulaPage(buildEditModeInjectScript())
1179
1180 formulaHtml.dispatchEvent(
1181 new window.PointerEvent('pointerdown', {
1182 bubbles: true,
1183 cancelable: true,
1184 button: 0,
1185 clientX: 180,
1186 clientY: 145,
1187 pointerId: 1
1188 })
1189 )
1190 formulaHtml.dispatchEvent(
1191 new window.PointerEvent('pointerup', {
1192 bubbles: true,
1193 cancelable: true,
1194 button: 0,
1195 clientX: 180,
1196 clientY: 145,
1197 pointerId: 1
1198 })
1199 )
1200
1201 const payload = readPayload(logs, EDIT_MODE_CONSOLE_PREFIX)
1202 expect(payload.selector).toBe('body[data-page-id="page"] [data-block-id="formula-1"]')
1203
1204 ;(
1205 window as Window & {
1206 __pptEditModeLiveUpdate: (
1207 selector: string,
1208 patch: { formula: { latex: string; html: string; displayMode: boolean } }
1209 ) => void
1210 }
1211 ).__pptEditModeLiveUpdate(payload.selector, {
1212 formula: {
1213 latex: 'x^3',
1214 html: '<span class="katex"><span class="katex-mathml"><math><semantics><mrow></mrow><annotation encoding="application/x-tex">x^3</annotation></semantics></math></span><span class="katex-html">x3</span></span>',
1215 displayMode: false
1216 }
1217 })
1218
1219 const updatedFormula = window.document.querySelector(
1220 'body[data-page-id="page"] [data-block-id="formula-1"]'
1221 )
1222 expect(updatedFormula).toBeTruthy()
1223 expect(updatedFormula?.classList.contains('katex')).toBe(true)
1224 expect(updatedFormula?.getAttribute('data-ppt-formula-latex')).toBe('x^3')
1225 })
1226
1227 it('does not treat surrounding text as latex when KaTeX has no annotation', () => {
1228 const { window, formulaHtml, logs } = setupFormulaWithoutAnnotationPage(
1229 buildEditModeInjectScript()
1230 )
1231
1232 formulaHtml.dispatchEvent(
1233 new window.PointerEvent('pointerdown', {
1234 bubbles: true,
1235 cancelable: true,
1236 button: 0,
1237 clientX: 180,
1238 clientY: 115,
1239 pointerId: 1
1240 })
1241 )
1242 formulaHtml.dispatchEvent(
1243 new window.PointerEvent('pointerup', {
1244 bubbles: true,
1245 cancelable: true,
1246 button: 0,
1247 clientX: 180,
1248 clientY: 115,
1249 pointerId: 1
1250 })
1251 )
1252
1253 const payload = readPayload(logs, EDIT_MODE_CONSOLE_PREFIX) as {
1254 snapshot?: { formula?: { latex?: string } }
1255 }
1256 expect(payload.snapshot?.formula).toBeUndefined()
1257 })
1258
1259 it('edit mode overlay uses tight KaTeX bounds instead of the inflated wrapper box', () => {
1260 const { window, formulaHtml } = setupDisplayFormulaPage(buildEditModeInjectScript())
1261
1262 formulaHtml.dispatchEvent(
1263 new window.PointerEvent('pointerdown', {
1264 bubbles: true,
1265 cancelable: true,
1266 button: 0,
1267 clientX: 240,
1268 clientY: 160,
1269 pointerId: 1
1270 })
1271 )
1272 formulaHtml.dispatchEvent(
1273 new window.PointerEvent('pointerup', {
1274 bubbles: true,
1275 cancelable: true,
1276 button: 0,
1277 clientX: 240,
1278 clientY: 160,
1279 pointerId: 1
1280 })
1281 )
1282
1283 const overlay = window.document.getElementById('arcsin1-presentation-editor-resize-overlay') as HTMLElement
1284 expect(overlay).toBeTruthy()
1285 expect(overlay.style.left).toBe('210px')
1286 expect(overlay.style.top).toBe('145px')
1287 expect(overlay.style.width).toBe('120px')
1288 expect(overlay.style.height).toBe('40px')
1289 })
1290
1291 it('inspector overlay keeps display formulas aligned to the rendered glyph bounds', () => {
1292 const { window, formulaHtml, logs } = setupDisplayFormulaPage(buildInspectorInjectScript())
1293
1294 formulaHtml.dispatchEvent(
1295 new window.MouseEvent('mousemove', {
1296 bubbles: true,
1297 cancelable: true,
1298 clientX: 240,
1299 clientY: 160
1300 })
1301 )
1302 formulaHtml.dispatchEvent(
1303 new window.MouseEvent('click', {
1304 bubbles: true,
1305 cancelable: true,
1306 clientX: 240,
1307 clientY: 160
1308 })
1309 )
1310
1311 const payload = readPayload(logs, INSPECTOR_CONSOLE_PREFIX)
1312 expect(payload.selector).not.toContain('formula-card')
1313 const overlay = window.document.getElementById('ppt-inspector-highlight-overlay') as HTMLElement
1314 expect(overlay).toBeTruthy()
1315 expect(overlay.style.left).toBe('206px')
1316 expect(overlay.style.top).toBe('141px')
1317 expect(overlay.style.width).toBe('128px')
1318 expect(overlay.style.height).toBe('48px')
1319 })
1320
1321 it('edit mode keeps non-formula overlays aligned to the selected element box', () => {
1322 const { window, card, logs } = setupOverflowBoxPage(buildEditModeInjectScript())
1323
1324 card.dispatchEvent(
1325 new window.PointerEvent('pointerdown', {
1326 bubbles: true,
1327 cancelable: true,
1328 button: 0,
1329 clientX: 220,
1330 clientY: 140,
1331 pointerId: 1
1332 })
1333 )
1334 card.dispatchEvent(
1335 new window.PointerEvent('pointerup', {
1336 bubbles: true,
1337 cancelable: true,
1338 button: 0,
1339 clientX: 220,
1340 clientY: 140,
1341 pointerId: 1
1342 })
1343 )
1344
1345 const payload = readPayload(logs, EDIT_MODE_CONSOLE_PREFIX) as {
1346 viewportBounds?: { width?: number; height?: number }
1347 }
1348 expect(payload.viewportBounds).toMatchObject({ width: 100, height: 40 })
1349 const overlay = window.document.getElementById('arcsin1-presentation-editor-resize-overlay') as HTMLElement
1350 expect(overlay).toBeTruthy()
1351 expect(overlay.style.left).toBe('180px')
1352 expect(overlay.style.top).toBe('120px')
1353 expect(overlay.style.width).toBe('100px')
1354 expect(overlay.style.height).toBe('40px')
1355 })
1356
1357 it('inspector keeps non-formula highlights aligned to the selected element box', () => {
1358 const { window, card, logs } = setupOverflowBoxPage(buildInspectorInjectScript())
1359
1360 card.dispatchEvent(
1361 new window.MouseEvent('mousemove', {
1362 bubbles: true,
1363 cancelable: true,
1364 clientX: 220,
1365 clientY: 140
1366 })
1367 )
1368 card.dispatchEvent(
1369 new window.MouseEvent('click', {
1370 bubbles: true,
1371 cancelable: true,
1372 clientX: 220,
1373 clientY: 140
1374 })
1375 )
1376
1377 const payload = readPayload(logs, INSPECTOR_CONSOLE_PREFIX) as {
1378 bounds?: { width?: number; height?: number }
1379 }
1380 expect(payload.bounds).toMatchObject({ width: 100, height: 40 })
1381 const overlay = window.document.getElementById('ppt-inspector-highlight-overlay') as HTMLElement
1382 expect(overlay).toBeTruthy()
1383 expect(overlay.style.left).toBe('176px')
1384 expect(overlay.style.top).toBe('116px')
1385 expect(overlay.style.width).toBe('108px')
1386 expect(overlay.style.height).toBe('48px')
1387 })
1388
1389 it('inspector keeps the selected highlight after hover moves away', () => {
1390 const { window, card, logs } = setupOverflowBoxPage(buildInspectorInjectScript())
1391
1392 card.dispatchEvent(
1393 new window.MouseEvent('mousemove', {
1394 bubbles: true,
1395 cancelable: true,
1396 clientX: 220,
1397 clientY: 140
1398 })
1399 )
1400 card.dispatchEvent(
1401 new window.MouseEvent('click', {
1402 bubbles: true,
1403 cancelable: true,
1404 clientX: 220,
1405 clientY: 140
1406 })
1407 )
1408
1409 const payload = readPayload(logs, INSPECTOR_CONSOLE_PREFIX) as {
1410 bounds?: { width?: number; height?: number }
1411 }
1412 expect(payload.bounds).toMatchObject({ width: 100, height: 40 })
1413
1414 const overlay = window.document.getElementById('ppt-inspector-highlight-overlay') as HTMLElement
1415 expect(overlay).toBeTruthy()
1416 expect(overlay.style.left).toBe('176px')
1417 expect(overlay.style.top).toBe('116px')
1418 expect(overlay.style.width).toBe('108px')
1419 expect(overlay.style.height).toBe('48px')
1420
1421 const mockedDocument = window.document as Document & {
1422 elementFromPoint: () => Element | null
1423 elementsFromPoint: () => Element[]
1424 }
1425 mockedDocument.elementFromPoint = () => null
1426 mockedDocument.elementsFromPoint = () => []
1427
1428 card.dispatchEvent(
1429 new window.MouseEvent('mousemove', {
1430 bubbles: true,
1431 cancelable: true,
1432 clientX: 12,
1433 clientY: 12
1434 })
1435 )
1436
1437 expect(overlay.style.left).toBe('176px')
1438 expect(overlay.style.top).toBe('116px')
1439 expect(overlay.style.width).toBe('108px')
1440 expect(overlay.style.height).toBe('48px')
1441 })
1442 })
1443
1443 lines TYPESCRIPT