Editor_test.res
1// test/Editor_test.res
2// End-to-end test for TipTap Editor integration
3
4open VitestHelpers
5
6@module("vitest") @val external vi: 'a = "vi"
7
8// Test component that inserts content via insertContent
9module TestInsertComponent = {
10 @jsx.component
11 let make = (~editor: Tiptap.t) => {
12 <button
13 className="insert-btn"
14 onClick={_ => {
15 // Create a paragraph node as JSON
16 let paragraphContent = JSON.parseExn(`{"type":"paragraph","content":[{"type":"text","text":"Inserted text"}]}`)
17 // Insert at current position
18 let _ = editor->Tiptap.Commands.insertContent(paragraphContent)
19 }}
20 >
21 {"Insert Content"->Preact.string}
22 </button>
23 }
24}
25
26// Test component that updates content via setContent
27module TestUpdateComponent = {
28 @jsx.component
29 let make = (~editor: Tiptap.t) => {
30 <button
31 className="update-btn"
32 onClick={_ => {
33 // Replace entire content
34 let newContent = JSON.parseExn(`{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Updated content"}]}]}`)
35 let _ = editor->Tiptap.Commands.setContent(newContent)
36 }}
37 >
38 {"Update Content"->Preact.string}
39 </button>
40 }
41}
42
43describe("TipTap Editor Integration", () => {
44 testAsync("should render editor with initial content", async t => {
45 // Create editor instance
46 // Note: When using Yjs Collaboration, initial content comes from the Yjs document,
47 // not the content option. The editor starts empty until content is added.
48 let editor = Tiptap.make({
49 extensions: Tiptap.extensions(Y.Doc.make(), "test"),
50 content: JSON.parseExn(`{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Initial content"}]}]}`),
51 })
52
53 // Verify editor was created
54 t->expect(editor)->toEqual(editor)
55 t->expect(editor->Tiptap.isEditable)->toEqual(true)
56 // With Yjs Collaboration, editor starts empty (content option is ignored)
57 // Content must be added via setContent or by initializing the Yjs document
58 t->expect(editor->Tiptap.isEmpty)->toEqual(true)
59 })
60
61 testAsync("should get content as JSON", async t => {
62 let editor = Tiptap.make({
63 extensions: Tiptap.extensions(Y.Doc.make(), "test"),
64 content: JSON.parseExn(`{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Test content"}]}]}`),
65 })
66
67 // Get content as JSON
68 let json = editor->Tiptap.getJSON
69
70 // Verify it's valid JSON (not null/undefined)
71 t->expect(json)->toEqual(json)
72 })
73
74 testAsync("should update content via setContent", async t => {
75 let editor = Tiptap.make({
76 extensions: Tiptap.extensions(Y.Doc.make(), "test"),
77 content: JSON.parseExn(`{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Initial"}]}]}`),
78 })
79
80 // Update content using Commands module
81 let newContent = JSON.parseExn(`{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Updated"}]}]}`)
82 let result = editor->Tiptap.Commands.setContent(newContent)
83
84 // setContent returns unit
85 t->expect(result)->toEqual()
86
87 // Verify editor still exists and is editable
88 t->expect(editor->Tiptap.isEditable)->toEqual(true)
89 })
90
91 testAsync("should insert content at current position", async t => {
92 let editor = Tiptap.make({
93 extensions: Tiptap.extensions(Y.Doc.make(), "test"),
94 content: JSON.parseExn(`{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Hello"}]}]}`),
95 })
96
97 // Insert content at current position using Commands module
98 let insertContent = JSON.parseExn(`{"type":"text","text":" World"}`)
99 let result = editor->Tiptap.Commands.insertContent(insertContent)
100
101 // insertContent returns unit
102 t->expect(result)->toEqual()
103 })
104
105 testAsync("should insert content at specific position", async t => {
106 let editor = Tiptap.make({
107 extensions: Tiptap.extensions(Y.Doc.make(), "test"),
108 content: JSON.parseExn(`{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"AB"}]}]}`),
109 })
110
111 // Insert at position 1 (between A and B) using Commands module
112 let insertContent = JSON.parseExn(`{"type":"text","text":"X"}`)
113 let result = editor->Tiptap.Commands.insertContentAt(1, insertContent)
114
115 // insertContentAt returns unit
116 t->expect(result)->toEqual()
117 })
118
119 testAsync("should handle editor lifecycle (destroy)", async t => {
120 let editor = Tiptap.make({
121 extensions: Tiptap.extensions(Y.Doc.make(), "test"),
122 content: JSON.parseExn(`{"type":"doc","content":[{"type":"paragraph"}]}`),
123 })
124
125 // Verify initial state
126 t->expect(editor->Tiptap.isDestroyed)->toEqual(false)
127
128 // Destroy the editor
129 editor->Tiptap.destroy
130
131 // Verify destroyed state
132 t->expect(editor->Tiptap.isDestroyed)->toEqual(true)
133 })
134})
135
136describe("Editor Component Integration", () => {
137 testAsync("should render Editor component with Content", async t => {
138 let disabled = Preact.Signal.make(false)
139
140 // Create the component structure
141 let component =
142 <Editor doc="test-doc-1" disabled={disabled}>
143 <Editor.Content />
144 </Editor>
145
146 // Verify component was created (not null/undefined)
147 t->expect(component)->toEqual(component)
148 })
149
150 testAsync("should render Editor with BubbleMenu", async t => {
151 let disabled = Preact.Signal.make(false)
152
153 let component =
154 <Editor doc="test-doc-2" disabled={disabled}>
155 <Editor.Content />
156 <Editor.BubbleMenu
157 options={{
158 placement: #top,
159 strategy: #fixed,
160 offset: Editor.FloatingUI.Pixels(10),
161 }}
162 >
163 <button> {"Bold"->Preact.string} </button>
164 </Editor.BubbleMenu>
165 </Editor>
166
167 t->expect(component)->toEqual(component)
168 })
169
170 testAsync("should render Editor with FloatingMenu", async t => {
171 let disabled = Preact.Signal.make(false)
172
173 let component =
174 <Editor doc="test-doc-3" disabled={disabled}>
175 <Editor.Content />
176 <Editor.FloatingMenu
177 options={{
178 placement: #"left-start",
179 }}
180 >
181 <button> {"Heading"->Preact.string} </button>
182 </Editor.FloatingMenu>
183 </Editor>
184
185 t->expect(component)->toEqual(component)
186 })
187})
188
189describe("Full Editor Workflow", () => {
190 testAsync("complete workflow: create -> set -> insert -> verify", async t => {
191 // Step 1: Create editor with initial content
192 let editor = Tiptap.make({
193 extensions: Tiptap.extensions(Y.Doc.make(), "test"),
194 content: JSON.parseExn(`{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Start"}]}]}`),
195 })
196
197 t->expect(editor->Tiptap.isEditable)->toEqual(true)
198
199 // Step 2: Set new content using Commands module
200 let content1 = JSON.parseExn(`{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"First update"}]}]}`)
201 let result1 = editor->Tiptap.Commands.setContent(content1)
202 t->expect(result1)->toEqual()
203
204 // Step 3: Insert additional content using Commands module
205 let content2 = JSON.parseExn(`{"type":"text","text":" + insert"}`)
206 let result2 = editor->Tiptap.Commands.insertContent(content2)
207 t->expect(result2)->toEqual()
208
209 // Step 4: Verify editor state
210 t->expect(editor->Tiptap.isEmpty)->toEqual(false)
211 t->expect(editor->Tiptap.isDestroyed)->toEqual(false)
212
213 // Step 5: Get JSON content and verify structure
214 let json = editor->Tiptap.getJSON
215 t->expect(json)->toEqual(json)
216
217 // Step 6: Test focus/blur using Commands module
218 let focusResult = editor->Tiptap.Commands.focus
219 t->expect(focusResult)->toEqual()
220
221 let blurResult = editor->Tiptap.Commands.blur
222 t->expect(blurResult)->toEqual()
223
224 // Step 7: Toggle editable state
225 editor->Tiptap.setEditable(false)
226 t->expect(editor->Tiptap.isEditable)->toEqual(false)
227
228 editor->Tiptap.setEditable(true)
229 t->expect(editor->Tiptap.isEditable)->toEqual(true)
230
231 // Step 8: Clean up
232 editor->Tiptap.destroy
233 t->expect(editor->Tiptap.isDestroyed)->toEqual(true)
234 })
235
236 testAsync("child component can access and modify editor", async t => {
237 let disabled = Preact.Signal.make(false)
238 let editor = Tiptap.make({
239 extensions: Tiptap.extensions(Y.Doc.make(), "test"),
240 content: JSON.parseExn(`{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Initial"}]}]}`),
241 })
242
243 // Create a component that will be a child of Editor and access editor
244 // Note: Editor component now manages its own instance, so we pass children separately
245 let testComponent =
246 <Editor doc="test-doc-4" disabled={disabled}>
247 <TestInsertComponent editor={editor} />
248 <TestUpdateComponent editor={editor} />
249 </Editor>
250
251 t->expect(testComponent)->toEqual(testComponent)
252 })
253})
254
255describe("Editor Waypoint Search Integration", () => {
256 testAsync("Editor renders correctly", async t => {
257 let disabled = Preact.Signal.make(false)
258
259 let component =
260 <Editor doc="test-doc-waypoint-1" disabled={disabled}>
261 <Editor.Content />
262 </Editor>
263
264 t->expect(component)->toEqual(component)
265 })
266
267 testAsync("Editor renders WaypointSearch when search is active", async t => {
268 let disabled = Preact.Signal.make(false)
269
270 // Component should be able to render WaypointSearch
271 let component =
272 <Editor doc="test-doc-waypoint-2" disabled={disabled}>
273 <Editor.Content />
274 </Editor>
275
276 t->expect(component)->toEqual(component)
277 })
278
279 testAsync("Editor works correctly", async t => {
280 let disabled = Preact.Signal.make(false)
281
282 let component =
283 <Editor doc="test-doc-waypoint-3" disabled={disabled}>
284 <Editor.Content />
285 </Editor>
286
287 t->expect(component)->toEqual(component)
288 })
289
290 testAsync("Editor component structure includes search integration", async t => {
291 let disabled = Preact.Signal.make(false)
292
293 // Test that Editor component has the expected structure for waypoint search
294 let component =
295 <Editor doc="test-doc-waypoint-4" disabled={disabled}>
296 <Editor.Content />
297 <Editor.BubbleMenu>
298 <button> {"Bold"->Preact.string} </button>
299 </Editor.BubbleMenu>
300 </Editor>
301
302 t->expect(component)->toEqual(component)
303 })
304})