import { findByText, getByText, queryByText } from '@testing-library/dom' import type { Command, EditorState } from 'prosemirror-state' import { vi } from 'vitest' import { createRichTextEditor } from './createRichTextEditor' import { testEditorState } from './fixtures/testState' describe('createRichTextEditor()', () => { const attributes = { 'aria-labelledby': 'label-text-123' } it('initializes an editor with the correct content', async () => { const node = document.createElement('div') const onChange = vi.fn() createRichTextEditor({ node, onChange, attributes, initialEditorState: testEditorState, }) const content = await findByText(node, 'Example content') expect(content.outerHTML).toBe('
Example content
') }) it('returns the expected API shape', async () => { const node = document.createElement('div') const onChange = vi.fn() const returnValue = createRichTextEditor({ node, onChange, attributes, initialEditorState: testEditorState, }) expect(Object.keys(returnValue)).toEqual(['destroy', 'dispatchTransaction', 'dom']) expect(typeof returnValue.destroy).toEqual('function') expect(typeof returnValue.dispatchTransaction).toEqual('function') }) it('destroys the instance', async () => { const node = document.createElement('div') const onChange = vi.fn() const { destroy } = createRichTextEditor({ node, onChange, attributes, initialEditorState: testEditorState, }) const content = await findByText(node, 'Example content') expect(content.outerHTML).toBe('Example content
') destroy() expect(queryByText(node, 'Example content')).toBe(null) }) it('updates the DOM when commands are dispatched', async () => { const node = document.createElement('div') const onChange = vi.fn() const command: Command = (state, dispatch) => { // Insert text at the current selection point, which is the start because // we don’t have a selection yet. if (!dispatch) return false dispatch(state.tr.insertText('Prepended content. ')) return true } const { dispatchTransaction } = createRichTextEditor({ node, onChange, attributes, initialEditorState: testEditorState, }) const content = await findByText(node, 'Example content') expect(content.outerHTML).toBe('Example content
') dispatchTransaction(command) expect(getByText(node, 'Prepended content. Example content').outerHTML).toBe( 'Prepended content. Example content
', ) }) it('calls onChange when the editor state changes', async () => { const node = document.createElement('div') const onChange = vi.fn() const command: Command = (state, dispatch) => { if (!dispatch) return false dispatch(state.tr.insertText('Prepended content. ')) return true } const { dispatchTransaction } = createRichTextEditor({ node, onChange, attributes, initialEditorState: testEditorState, }) expect(onChange).not.toHaveBeenCalled() dispatchTransaction(command) expect(onChange).toHaveBeenCalled() }) it('calls onChange with the updated state', async () => { const node = document.createElement('div') const onChange = vi.fn() const command: Command = (state, dispatch) => { if (!dispatch) return false dispatch(state.tr.insertText('Prepended content. ')) return true } const { dispatchTransaction } = createRichTextEditor({ node, onChange, attributes, initialEditorState: testEditorState, }) dispatchTransaction(command) const expectedResult = { doc: { content: [ { content: [ { text: 'Prepended content. Example content', type: 'text', }, ], type: 'paragraph', }, ], type: 'doc', }, selection: { anchor: 20, head: 20, type: 'text', }, } const updatedEditorState = onChange.mock.calls[0][0] as EditorState expect(updatedEditorState.toJSON()).toStrictEqual(expectedResult) }) it('defaults to editable', async () => { const node = document.createElement('div') const onChange = vi.fn() createRichTextEditor({ node, onChange, attributes, initialEditorState: testEditorState, }) expect(node.children[0]?.getAttribute('contenteditable')).toBe('true') }) it('respects initial isEditable value', async () => { const node = document.createElement('div') const onChange = vi.fn() createRichTextEditor({ node, onChange, attributes, initialEditorState: testEditorState, isEditable: () => false, }) expect(node.children[0]?.getAttribute('contenteditable')).toBe('false') }) it('updates editable status', async () => { let editable = true const node = document.createElement('div') const onChange = vi.fn() const noopCommand: Command = (state, dispatch) => { if (!dispatch) return false dispatch(state.tr) return true } const { dispatchTransaction } = createRichTextEditor({ node, onChange, attributes, initialEditorState: testEditorState, isEditable: () => editable, }) expect(node.children[0]?.getAttribute('contenteditable')).toBe('true') editable = false dispatchTransaction(noopCommand) expect(node.children[0]?.getAttribute('contenteditable')).toBe('false') }) it('aria-labelledby is present', async () => { const editable = true const node = document.createElement('div') const onChange = vi.fn() const noopCommand: Command = (state, dispatch) => { if (!dispatch) return false dispatch(state.tr) return true } const { dispatchTransaction } = createRichTextEditor({ node, onChange, attributes, initialEditorState: testEditorState, isEditable: () => editable, }) dispatchTransaction(noopCommand) expect(node.children[0]?.getAttribute('aria-labelledby')).toBe('label-text-123') }) })