virt2/api/soft/CodeMirror/view/test/test-draw.ts

230 lines
8.5 KiB
TypeScript
Executable File

import {tempEditor} from "./temp-editor"
import {EditorSelection} from "@codemirror/next/state"
import {EditorView} from "@codemirror/next/view"
import ist from "ist"
function domText(view: EditorView) {
let text = "", eol = false
function scan(node: Node) {
if (node.nodeType == 1) {
if (node.nodeName == "BR" || (node as HTMLElement).contentEditable == "false") return
if (eol) { text += "\n"; eol = false }
for (let ch = node.firstChild as (Node | null); ch; ch = ch.nextSibling) scan(ch)
eol = true
} else if (node.nodeType == 3) {
text += node.nodeValue
}
}
scan(view.contentDOM)
return text
}
describe("EditorView drawing", () => {
it("follows updates to the document", () => {
let cm = tempEditor("one\ntwo")
ist(domText(cm), "one\ntwo")
cm.dispatch(cm.state.t().replace(1, 2, "x"))
ist(domText(cm), "oxe\ntwo")
cm.dispatch(cm.state.t().replace(2, 5, ["1", "2", "3"]))
ist(domText(cm), "ox1\n2\n3wo")
cm.dispatch(cm.state.t().replace(1, 8, ""))
ist(domText(cm), "oo")
})
it("works in multiple lines", () => {
let doc = "abcdefghijklmnopqrstuvwxyz\n".repeat(10)
let cm = tempEditor("")
cm.dispatch(cm.state.t().replace(0, 0, doc))
ist(domText(cm), doc)
cm.dispatch(cm.state.t().replace(0, 0, "/"))
doc = "/" + doc
ist(domText(cm), doc)
cm.dispatch(cm.state.t().replace(100, 104, "$"))
doc = doc.slice(0, 100) + "$" + doc.slice(104)
ist(domText(cm), doc)
cm.dispatch(cm.state.t().replace(200, 268, ""))
doc = doc.slice(0, 200)
ist(domText(cm), doc)
})
it("can split a line", () => {
let cm = tempEditor("abc\ndef\nghi")
cm.dispatch(cm.state.t().replace(4, 4, "xyz\nk"))
ist(domText(cm), "abc\nxyz\nkdef\nghi")
})
it("redraws lazily", () => {
let cm = tempEditor("one\ntwo\nthree")
let line0 = cm.domAtPos(0).node, line1 = line0.nextSibling!, line2 = line1.nextSibling!
let text0 = line0.firstChild!, text2 = line2.firstChild!
cm.dispatch(cm.state.t().replace(5, 5, "x"))
ist(text0.parentElement, line0)
ist(cm.contentDOM.contains(line0))
ist(cm.contentDOM.contains(line1))
ist(text2.parentElement, line2)
ist(cm.contentDOM.contains(line2))
})
it("notices the doc needs to be redrawn when only inserting empty lines", () => {
let cm = tempEditor("")
cm.dispatch(cm.state.t().replace(0, 0, "\n\n\n"))
ist(domText(cm), "\n\n\n")
})
it("draws BR nodes on empty lines", () => {
let cm = tempEditor("one\n\ntwo")
let emptyLine = cm.domAtPos(4).node
ist(emptyLine.childNodes.length, 1)
ist(emptyLine.firstChild!.nodeName, "BR")
cm.dispatch(cm.state.t().replace(4, 4, "x"))
ist(!Array.from(cm.domAtPos(4).node.childNodes).some(n => (n as any).nodeName == "BR"))
})
it("only draws visible content", () => {
let cm = tempEditor("a\n".repeat(500) + "b\n".repeat(500), [], {scroll: 300})
cm.scrollDOM.scrollTop = 3000
cm.measure()
ist(cm.contentDOM.childNodes.length, 500, "<")
ist(cm.contentDOM.scrollHeight, 10000, ">")
ist(!cm.contentDOM.textContent!.match(/b/))
let gap = cm.contentDOM.lastChild
cm.dispatch(cm.state.t().replace(2000, 2000, "\n\n"))
ist(cm.contentDOM.lastChild, gap) // Make sure gap nodes are reused when resized
cm.scrollDOM.scrollTop = cm.scrollDOM.scrollHeight / 2
cm.measure()
ist(cm.contentDOM.textContent!.match(/b/))
})
it("keeps a drawn area around selection ends", () => {
let cm = tempEditor("\nsecond\n" + "x\n".repeat(500) + "last", [], {scroll: 300})
cm.dispatch(cm.state.t().setSelection(EditorSelection.single(1, cm.state.doc.length)))
cm.focus()
let text = cm.contentDOM.textContent!
ist(text.length, 500, "<")
ist(/second/.test(text))
ist(/last/.test(text))
})
it("can handle replace-all like events", () => {
let content = "", chars = "abcdefghijklmn \n"
for (let i = 0; i < 5000; i++) content += chars[Math.floor(Math.random() * chars.length)]
let cm = tempEditor(content), tr = cm.state.t()
for (let i = Math.floor(content.length / 100); i >= 0; i--) {
let from = Math.floor(Math.random() * (tr.doc.length - 10)), to = from + Math.floor(Math.random() * 10)
tr.replace(from, to, "XYZ")
content = content.slice(0, from) + "XYZ" + content.slice(to)
}
ist(tr.doc.toString(), content)
cm.dispatch(tr)
ist(domText(cm), content.slice(cm.viewport.from, cm.viewport.to))
})
it("can handle deleting a line's content", () => {
let cm = tempEditor("foo\nbaz")
cm.dispatch(cm.state.t().replace(4, 7, ""))
ist(domText(cm), "foo\n")
})
it("can insert blank lines at the end of the document", () => {
let cm = tempEditor("foo")
cm.dispatch(cm.state.t().replace(3, 3, "\n\nx"))
ist(domText(cm), "foo\n\nx")
})
it("can handle deleting the end of a line", () => {
let cm = tempEditor("a\nbc\n")
cm.dispatch(cm.state.t().replace(3, 4, ""))
cm.dispatch(cm.state.t().replace(3, 3, "d"))
ist(domText(cm), "a\nbd\n")
})
it("correctly handles very complicated transactions", () => {
let doc = "foo\nbar\nbaz", chars = "abcdef \n"
let cm = tempEditor(doc)
for (let i = 0; i < 10; i++) {
let tr = cm.state.t(), pos = Math.min(20, doc.length)
for (let j = 0; j < 1; j++) {
let choice = Math.random(), r = Math.random()
if (choice < 0.15) {
pos = Math.min(doc.length, Math.max(0, pos + 5 - Math.floor(r * 10)))
} else if (choice < 0.5) {
let from = Math.max(0, pos - Math.floor(r * 2)), to = Math.min(doc.length, pos + Math.floor(r * 4))
tr.replace(from, to, "")
doc = doc.slice(0, from) + doc.slice(to)
pos = from
} else {
let text = ""
for (let k = Math.floor(r * 6); k >= 0; k--) text += chars[Math.floor(chars.length * Math.random())]
tr.replace(pos, pos, text)
doc = doc.slice(0, pos) + text + doc.slice(pos)
pos += text.length
}
}
cm.dispatch(tr)
ist(domText(cm), doc.slice(cm.viewport.from, cm.viewport.to))
}
})
function later() {
return new Promise(resolve => setTimeout(resolve, 50))
}
it("notices it is added to the DOM even if initially detached", () => {
if (!(window as any).IntersectionObserver) return // Only works with intersection observer support
let cm = tempEditor("a\n\b\nc\nd", [EditorView.contentAttributes.of({style: "font-size: 60px"})])
let parent = cm.dom.parentNode!
cm.dom.remove()
return later().then(() => {
parent.appendChild(cm.dom)
return later().then(() => {
ist(cm.contentHeight, 200, ">")
})
})
})
it("hides parts of long lines that are horizontally out of view", () => {
let cm = tempEditor("one\ntwo\n?" + "three ".repeat(3333) + "!\nfour")
let {node} = cm.domAtPos(9)
ist(node.nodeValue!.length, 2e4, "<")
ist(node.nodeValue!.indexOf("!"), -1)
ist(cm.scrollDOM.scrollWidth, cm.defaultCharacterWidth * 1.6e4, ">")
cm.scrollDOM.scrollLeft = cm.scrollDOM.scrollWidth
cm.measure()
;({node} = cm.domAtPos(20007)!)
ist(node.nodeValue!.length, 2e4, "<")
ist(node.nodeValue!.indexOf("!"), -1, ">")
ist(cm.scrollDOM.scrollWidth, cm.defaultCharacterWidth * 1.6e4, ">")
})
it("hides parts of long lines that are vertically out of view", () => {
let cm = tempEditor("<" + "long line ".repeat(4e3) + ">", [], {scroll: 100, wrapping: true})
let {node} = cm.domAtPos(1)
ist(node.nodeValue!.length, cm.state.doc.length, "<")
ist(node.nodeValue!.indexOf("<"), -1, ">")
cm.scrollDOM.scrollTop = cm.scrollDOM.scrollHeight / 2
cm.measure()
let rect = cm.scrollDOM.getBoundingClientRect()
;({node} = cm.domAtPos(cm.posAtCoords({x: (rect.left + rect.right) / 2, y: (rect.top + rect.bottom) / 2})))
ist(node.nodeValue!.length, cm.state.doc.length, "<")
ist(node.nodeValue!.indexOf("<"), -1)
ist(node.nodeValue!.indexOf(">"), -1)
cm.scrollDOM.scrollTop = cm.scrollDOM.scrollHeight
cm.measure()
;({node} = cm.domAtPos(cm.state.doc.length - 1))
ist(node.nodeValue!.length, cm.state.doc.length, "<")
ist(node.nodeValue!.indexOf(">"), -1, ">")
})
it("properly attaches styles in shadow roots", () => {
let ws = document.querySelector("#workspace")!
let wrap = ws.appendChild(document.createElement("div"))
if (!wrap.attachShadow) return
let shadow = wrap.attachShadow({mode: "open"})
let editor = new EditorView({root: shadow})
shadow.appendChild(editor.dom)
editor.measure()
ist(getComputedStyle(editor.dom).display, "flex")
wrap.remove()
})
})