Files
upstream-mirror 083a46e93b
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:11:02 +05:00

296 lines
7.6 KiB
TypeScript

import { Editor } from '@tiptap/core'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCaret from '@tiptap/extension-collaboration-caret'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import { Table, TableCell, TableHeader, TableRow } from '@tiptap/extension-table'
import Text from '@tiptap/extension-text'
import { describe, expect, it } from 'vite-plus/test'
import * as Y from 'yjs'
/**
* Tests for CollaborationCaret extension, especially edge cases
* with initial HTML content and complex structures like tables.
*/
describe('extension-collaboration-caret', () => {
const editorElClass = 'tiptap'
const createEditorEl = () => {
const editorEl = document.createElement('div')
editorEl.classList.add(editorElClass)
document.body.appendChild(editorEl)
return editorEl
}
const getEditorEl = () => document.querySelector(`.${editorElClass}`)
/**
* Test that the editor can be initialized with HTML content containing tables
* when using Collaboration and CollaborationCaret extensions.
*
* This test reproduces the issue from GitHub #6979 where the editor crashes
* with "Cannot read properties of undefined (reading 'doc')" when initialized
* with HTML content containing tables.
*/
it('should not crash when initialized with HTML content containing tables', () => {
const ydoc = new Y.Doc()
// Create a mock provider with minimal awareness API
const mockProvider = {
awareness: {
states: new Map(),
setLocalStateField: () => {},
on: () => {},
off: () => {},
getStates: () => new Map(),
},
}
const editorEl = createEditorEl()
// This should not throw an error
expect(() => {
const editor = new Editor({
element: editorEl,
extensions: [
Document,
Paragraph,
Text,
Table,
TableRow,
TableHeader,
TableCell,
Collaboration.configure({
document: ydoc,
}),
CollaborationCaret.configure({
provider: mockProvider,
user: {
name: 'Test User',
color: '#ff0000',
},
}),
],
content: `
<table>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
<tr>
<td>Cell 1</td>
<td>Cell 2</td>
</tr>
</table>
`,
})
editor.destroy()
}).not.toThrow()
getEditorEl()?.remove()
})
/**
* Test that the editor can be initialized with simple HTML content
* when using Collaboration and CollaborationCaret extensions.
*/
it('should not crash when initialized with simple HTML content', () => {
const ydoc = new Y.Doc()
const mockProvider = {
awareness: {
states: new Map(),
setLocalStateField: () => {},
on: () => {},
off: () => {},
getStates: () => new Map(),
},
}
const editorEl = createEditorEl()
expect(() => {
const editor = new Editor({
element: editorEl,
extensions: [
Document,
Paragraph,
Text,
Collaboration.configure({
document: ydoc,
}),
CollaborationCaret.configure({
provider: mockProvider,
user: {
name: 'Test User',
color: '#ff0000',
},
}),
],
content: '<p>Hello world</p>',
})
editor.destroy()
}).not.toThrow()
getEditorEl()?.remove()
})
/**
* Regression test: when an editor is destroyed but the provider/awareness
* lives on (e.g. multiple editors sharing one provider), the awareness
* 'update' listener registered by CollaborationCaret must be removed.
* Otherwise the closure keeps the destroyed editor reachable and leaks memory.
*/
it('should remove its awareness listener when the editor is destroyed', () => {
const ydoc = new Y.Doc()
const updateListeners = new Set<(...args: any[]) => void>()
const mockAwareness = {
states: new Map(),
setLocalStateField: () => {},
on: (event: string, fn: (...args: any[]) => void) => {
if (event === 'update') updateListeners.add(fn)
},
off: (event: string, fn: (...args: any[]) => void) => {
if (event === 'update') updateListeners.delete(fn)
},
getStates: () => new Map(),
}
const mockProvider = { awareness: mockAwareness }
const editorEl = createEditorEl()
const editor = new Editor({
element: editorEl,
extensions: [
Document,
Paragraph,
Text,
Collaboration.configure({ document: ydoc }),
CollaborationCaret.configure({
provider: mockProvider,
user: { name: 'Test User', color: '#ff0000' },
}),
],
})
expect(updateListeners.size).toBe(1)
editor.destroy()
expect(updateListeners.size).toBe(0)
getEditorEl()?.remove()
})
/**
* Test that the editor can be initialized without any content
* when using Collaboration and CollaborationCaret extensions.
*/
it('should work correctly when initialized without content', () => {
const ydoc = new Y.Doc()
const mockProvider = {
awareness: {
states: new Map(),
setLocalStateField: () => {},
on: () => {},
off: () => {},
getStates: () => new Map(),
},
}
const editorEl = createEditorEl()
expect(() => {
const editor = new Editor({
element: editorEl,
extensions: [
Document,
Paragraph,
Text,
Collaboration.configure({
document: ydoc,
}),
CollaborationCaret.configure({
provider: mockProvider,
user: {
name: 'Test User',
color: '#ff0000',
},
}),
],
})
editor.destroy()
}).not.toThrow()
getEditorEl()?.remove()
})
it('should handle null or undefined awareness state values without crashing', () => {
const ydoc = new Y.Doc()
const states = new Map<number, Record<string, any> | null>()
states.set(1, { user: { name: 'Alice', color: '#ff0000' } })
states.set(2, null as any)
states.set(3, undefined as any)
states.set(4, { user: { name: 'Bob', color: '#00ff00' } })
const mockProvider = {
awareness: {
states,
setLocalStateField: () => {},
on: () => {},
off: () => {},
getStates: () => states,
},
}
const editorEl = createEditorEl()
const editor = new Editor({
element: editorEl,
extensions: [
Document,
Paragraph,
Text,
Collaboration.configure({
document: ydoc,
}),
CollaborationCaret.configure({
provider: mockProvider,
user: {
name: 'Test User',
color: '#0000ff',
},
}),
],
})
const users = editor.storage.collaborationCaret.users
expect(users).toEqual(
expect.arrayContaining([
expect.objectContaining({ clientId: 1, name: 'Alice', color: '#ff0000' }),
expect.objectContaining({ clientId: 4, name: 'Bob', color: '#00ff00' }),
]),
)
const nullUser = users.find((u: any) => u.clientId === 2)
expect(nullUser).toEqual({ clientId: 2 })
const undefinedUser = users.find((u: any) => u.clientId === 3)
expect(undefinedUser).toEqual({ clientId: 3 })
editor.destroy()
getEditorEl()?.remove()
})
})