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})