返回 oh-my-ppt
element-insertion.test.ts
根目录 / tests / unit / html-editor / element-insertion.test.ts
1 import { beforeEach, describe, expect, it, vi } from 'vitest'
2 import type { HtmlEditorCanvasHandle } from '../../../src/renderer/src/components/html-editor/HtmlEditorCanvas'
3 import { useHtmlElementInsertion } from '../../../src/renderer/src/components/html-editor/useHtmlElementInsertion'
4 import { ART_TEXT_TEMPLATES } from '../../../src/renderer/src/lib/artTextTemplates'
5 import { useHtmlEditHistoryStore } from '../../../src/renderer/src/store/htmlEditHistoryStore'
6 import { useHtmlEditStore } from '../../../src/renderer/src/store/htmlEditStore'
7 import type { EditableElementSnapshot, EditSelectionPayload } from '@arcsin1/presentation-editor-runtime'
8
9 const PAGE_CONTEXT = {
10 pageId: 'page-1',
11 htmlPath: '/tmp/page-1.html',
12 sessionId: 'session-1'
13 }
14
15 function createSnapshot(selector: string): EditableElementSnapshot {
16 return {
17 selector,
18 label: 'Inserted element',
19 elementTag: 'div',
20 elementText: '',
21 kind: 'unknown',
22 capabilities: ['layout', 'layer'],
23 metrics: {
24 viewport: { x: 0, y: 0, width: 100, height: 100 },
25 page: { x: 0, y: 0, width: 100, height: 100 },
26 translateX: 0,
27 translateY: 0
28 },
29 computed: {
30 zIndex: '20',
31 opacity: '1',
32 backgroundColor: 'transparent',
33 color: '#34402c'
34 },
35 inline: {},
36 attrs: {}
37 }
38 }
39
40 describe('useHtmlElementInsertion', () => {
41 beforeEach(() => {
42 useHtmlEditHistoryStore.getState().clear()
43 useHtmlEditStore.getState().reset()
44 })
45
46 it('uses z-index 20 for every newly inserted element, including later inserts', async () => {
47 const injectedFragments: string[] = []
48 const iframe = {
49 injectElement: vi.fn((_parentSelector: string, htmlFragment: string) => {
50 injectedFragments.push(htmlFragment)
51 return true
52 }),
53 readElementSnapshot: vi.fn(async (selector: string) => createSnapshot(selector)),
54 ensureChartJs: vi.fn(async () => true)
55 } as unknown as HtmlEditorCanvasHandle
56
57 useHtmlEditStore.getState().attach({
58 t: () => 'New text',
59 requestRefresh: vi.fn(),
60 bumpThumbnail: vi.fn(),
61 getPageContext: () => PAGE_CONTEXT
62 })
63 useHtmlEditStore.getState().setIframeHandle(iframe)
64
65 const insertion = useHtmlElementInsertion({ designWidth: 1280, t: () => 'New text' })
66 await insertion.addText()
67 await insertion.addArtText(ART_TEXT_TEMPLATES[0].id)
68 await insertion.addShape('rect')
69 await insertion.addIcon('lightbulb')
70 await insertion.addChart('bar')
71
72 expect(injectedFragments).toHaveLength(5)
73 injectedFragments.forEach((fragment) => {
74 expect(fragment).toMatch(/z-index:20(?:;|\")/)
75 })
76 })
77
78 it('inserts image and video elements with escaped media URLs', async () => {
79 const injectedFragments: string[] = []
80 const iframe = {
81 injectElement: vi.fn((_parentSelector: string, htmlFragment: string) => {
82 injectedFragments.push(htmlFragment)
83 return true
84 }),
85 readElementSnapshot: vi.fn(async (selector: string) => createSnapshot(selector))
86 } as unknown as HtmlEditorCanvasHandle
87
88 useHtmlEditStore.getState().attach({
89 t: () => 'New text',
90 requestRefresh: vi.fn(),
91 bumpThumbnail: vi.fn(),
92 getPageContext: () => PAGE_CONTEXT
93 })
94 useHtmlEditStore.getState().setIframeHandle(iframe)
95
96 const insertion = useHtmlElementInsertion({ designWidth: 1280, t: () => 'New text' })
97 await insertion.addImage('https://cdn.example.com/photo?a=1&b=2')
98 await insertion.addVideo('file:///tmp/demo%20video.mp4')
99
100 expect(injectedFragments).toHaveLength(2)
101 expect(injectedFragments[0]).toContain('<img')
102 expect(injectedFragments[0]).toContain('src="https://cdn.example.com/photo?a=1&amp;b=2"')
103 expect(injectedFragments[0]).toContain('width:480px')
104 expect(injectedFragments[1]).toContain('<video')
105 expect(injectedFragments[1]).toContain('controls')
106 expect(injectedFragments[1]).toContain('preload="metadata"')
107 expect(injectedFragments[1]).toContain('width:640px')
108 })
109
110 it('does not save a media addition when the canvas rejects the injection', async () => {
111 const iframe = {
112 injectElement: vi.fn(async () => false),
113 readElementSnapshot: vi.fn(async (selector: string) => createSnapshot(selector))
114 } as unknown as HtmlEditorCanvasHandle
115
116 useHtmlEditStore.getState().attach({
117 t: () => 'New text',
118 requestRefresh: vi.fn(),
119 bumpThumbnail: vi.fn(),
120 getPageContext: () => PAGE_CONTEXT
121 })
122 useHtmlEditStore.getState().setIframeHandle(iframe)
123
124 const insertion = useHtmlElementInsertion({ designWidth: 1280, t: () => 'New text' })
125
126 await expect(insertion.addImage('https://cdn.example.com/photo.webp')).resolves.toBe(false)
127 expect(useHtmlEditHistoryStore.getState().addElements).toHaveLength(0)
128 })
129 })
130
131 describe('useHtmlEditStore in-flight flow resize', () => {
132 beforeEach(() => {
133 useHtmlEditHistoryStore.getState().clear()
134 useHtmlEditStore.getState().reset()
135 })
136
137 it('retains geometry from the canvas before the moved event reaches history', async () => {
138 const selector = 'body[data-page-id="page-1"] [data-block-id="flow"]'
139 const iframe = {
140 readElementLayout: vi.fn(async () => ({
141 isAbsoluteMode: false,
142 x: 24,
143 y: -12,
144 width: 150,
145 height: 75,
146 visualX: 24,
147 visualY: -12
148 }))
149 } as unknown as HtmlEditorCanvasHandle
150 useHtmlEditStore.getState().attach({
151 t: () => 'New text',
152 requestRefresh: vi.fn(),
153 bumpThumbnail: vi.fn(),
154 getPageContext: () => PAGE_CONTEXT
155 })
156 useHtmlEditStore.getState().setIframeHandle(iframe)
157 useHtmlEditStore.setState({
158 selection: {
159 selector,
160 label: selector,
161 elementTag: 'div',
162 elementText: '',
163 isText: false,
164 style: {},
165 translateX: 0,
166 translateY: 0,
167 snapshot: {
168 selector,
169 label: selector,
170 elementTag: 'div',
171 elementText: '',
172 kind: 'container',
173 capabilities: ['layout'],
174 metrics: {
175 viewport: { x: 0, y: 0, width: 100, height: 100 },
176 page: { x: 0, y: 0, width: 100, height: 100 },
177 translateX: 0,
178 translateY: 0
179 },
180 computed: {},
181 inline: {},
182 attrs: {}
183 }
184 } as EditSelectionPayload
185 })
186
187 await useHtmlEditStore.getState().flushPendingDrags()
188
189 expect(useHtmlEditHistoryStore.getState().getSnapshotForPage(PAGE_CONTEXT.pageId).dragEdits).toEqual([
190 expect.objectContaining({
191 selector,
192 x: 24,
193 y: -12,
194 width: 150,
195 height: 75
196 })
197 ])
198 })
199 })
200
200 lines TYPESCRIPT