godo.js/tests/helpers.mjs

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