120 lines
3.7 KiB
JavaScript
120 lines
3.7 KiB
JavaScript
import {EditorState, Selection, TextSelection} from 'prosemirror-state'
|
|
import {DOMParser, DOMSerializer} from 'prosemirror-model'
|
|
import {history} from 'prosemirror-history'
|
|
import {vi, test} from 'vitest'
|
|
|
|
import {loadSchema} from '../src-js/utils'
|
|
import {buildKeymap} from '../src-js/utils.mjs'
|
|
import {blocks, marks} from '../src-js/godo-menus.mjs'
|
|
|
|
export function parse (schema, html) {
|
|
const parser = DOMParser.fromSchema(schema)
|
|
let div = document.createElement('div')
|
|
div.innerHTML = html.trim()
|
|
return parser.parse(div)
|
|
}
|
|
|
|
export function serialize (schema, fragment) {
|
|
const div = document.createElement('div')
|
|
const dom = DOMSerializer
|
|
.fromSchema(schema)
|
|
.serializeFragment(fragment)
|
|
div.appendChild(dom)
|
|
return div.innerHTML
|
|
}
|
|
|
|
const viewMock = {
|
|
focus: vi.fn(),
|
|
}
|
|
|
|
export class TestState {
|
|
constructor (components, initialHtml) {
|
|
const schema = loadSchema(components)
|
|
const doc = parse(schema, initialHtml || '')
|
|
|
|
this.schema = schema
|
|
this.keymap = buildKeymap(components, this.schema, null, {})
|
|
this.marksMenu = marks(components, this.schema).items
|
|
this.blocksMenu = blocks(components, this.schema).items
|
|
|
|
this.state = EditorState.create({
|
|
schema,
|
|
doc,
|
|
plugins: [history()],
|
|
})
|
|
|
|
this.apply(this.state.tr.setSelection(Selection.atEnd(doc)))
|
|
}
|
|
|
|
get html () { return serialize(this.schema, this.state.doc) }
|
|
|
|
apply (tr) { this.state = this.state.apply(tr) }
|
|
|
|
type (text) { this.apply(this.state.tr.insertText(text)) }
|
|
|
|
resolveTextOffset (node, textOffset) {
|
|
if (node.isText) {
|
|
return textOffset + node.marks.length
|
|
}
|
|
|
|
// 1 for node start token, don't count document start
|
|
let resolvedOffset = node.type === this.schema.nodes.doc ? 0 : 1
|
|
|
|
for (let childId = 0; childId < node.childCount; ++childId) {
|
|
const child = node.child(childId)
|
|
const childText = child.textContent
|
|
if (textOffset <= childText.length) {
|
|
return resolvedOffset + this.resolveTextOffset(child, textOffset)
|
|
}
|
|
textOffset -= childText.length
|
|
resolvedOffset += child.nodeSize
|
|
}
|
|
}
|
|
|
|
selectTextOffset (start, end) {
|
|
const transaction = this.state.tr
|
|
const doc = transaction.doc
|
|
let selectionStart = this.resolveTextOffset(doc, start)
|
|
let selectionEnd = end !== undefined ? this.resolveTextOffset(doc, end) : undefined
|
|
const newSelection = TextSelection.create(doc, selectionStart, selectionEnd)
|
|
this.apply(transaction.setSelection(newSelection))
|
|
}
|
|
|
|
select (text) {
|
|
const textOffset = this.state.doc.textContent.indexOf(text)
|
|
if (textOffset === -1) throw new Error('Can\'t find given text in document')
|
|
this.selectTextOffset(textOffset, textOffset + text.length)
|
|
}
|
|
|
|
cursorAfter (text) {
|
|
const textOffset = this.state.doc.textContent.indexOf(text)
|
|
if (textOffset === -1) throw new Error('Can\'t find given text in document')
|
|
this.selectTextOffset(textOffset + text.length)
|
|
}
|
|
|
|
key (keyScheme) { this.keymap[keyScheme](this.state, tr => this.apply(tr)) }
|
|
|
|
clickBlocksMenu (name) { this.blocksMenu[name].run(this.state, tr => this.apply(tr)) }
|
|
|
|
clickMarksMenu (name) { this.marksMenu[name].run(this.state, tr => this.apply(tr), viewMock) }
|
|
}
|
|
|
|
export const domTest = test.extend({
|
|
// Empty {} is required by vitest
|
|
// eslint-disable-next-line no-empty-pattern
|
|
appendToDom: async ({}, use) => {
|
|
const wrappers = []
|
|
const appendToDom = (htmlContent) => {
|
|
const wrapper = document.createElement('div')
|
|
wrapper.innerHTML = htmlContent
|
|
document.appendChild(wrapper)
|
|
wrappers.push(wrapper)
|
|
return wrapper
|
|
}
|
|
await use(appendToDom)
|
|
for (const wrapper of wrappers) {
|
|
wrapper.remove()
|
|
}
|
|
},
|
|
})
|