first commit
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import sinon from 'sinon'
|
||||
import { waitFor } from '@testing-library/react'
|
||||
import { expect } from 'chai'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { EditorSelection, EditorState } from '@codemirror/state'
|
||||
import {
|
||||
cursorPosition,
|
||||
restoreCursorPosition,
|
||||
} from '../../../../../frontend/js/features/source-editor/extensions/cursor-position'
|
||||
|
||||
const doc = `
|
||||
\\documentclass{article}
|
||||
\\title{Your Paper}
|
||||
\\author{You}
|
||||
\\begin{document}
|
||||
\\maketitle
|
||||
\\begin{abstract}
|
||||
Your abstract.
|
||||
\\end{abstract}
|
||||
\\section{Introduction}
|
||||
Your introduction goes here!
|
||||
\\end{document}`
|
||||
|
||||
const mockDoc = () => {
|
||||
return {
|
||||
doc_id: 'test-doc',
|
||||
}
|
||||
}
|
||||
|
||||
describe('CodeMirror cursor position extension', function () {
|
||||
afterEach(function () {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('stores cursor position when the view is destroyed', async function () {
|
||||
const currentDoc = mockDoc()
|
||||
|
||||
sinon.stub(window.Storage.prototype, 'getItem').callsFake(key => {
|
||||
switch (key) {
|
||||
case 'doc.position.test-doc':
|
||||
return JSON.stringify({
|
||||
cursorPosition: { row: 1, column: 1 },
|
||||
firstVisibleLine: 5,
|
||||
})
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const setItem = sinon.spy(window.Storage.prototype, 'setItem')
|
||||
|
||||
const view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc,
|
||||
extensions: [cursorPosition({ currentDoc })],
|
||||
}),
|
||||
})
|
||||
|
||||
view.dispatch({
|
||||
selection: EditorSelection.cursor(50),
|
||||
})
|
||||
|
||||
view.destroy()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setItem).to.have.been.calledWith(
|
||||
'doc.position.test-doc',
|
||||
JSON.stringify({
|
||||
cursorPosition: {
|
||||
row: 3,
|
||||
column: 6,
|
||||
},
|
||||
firstVisibleLine: 5,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('restores cursor position', async function () {
|
||||
const currentDoc = mockDoc()
|
||||
|
||||
const getItem = sinon
|
||||
.stub(window.Storage.prototype, 'getItem')
|
||||
.callsFake(key => {
|
||||
switch (key) {
|
||||
case 'doc.position.test-doc':
|
||||
return JSON.stringify({
|
||||
cursorPosition: { row: 3, column: 5 },
|
||||
firstVisibleLine: 0,
|
||||
})
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc,
|
||||
extensions: [cursorPosition({ currentDoc })],
|
||||
}),
|
||||
})
|
||||
view.dispatch(restoreCursorPosition(view.state.doc, 'test-doc'))
|
||||
|
||||
expect(getItem).to.have.been.calledWith('doc.position.test-doc')
|
||||
|
||||
await waitFor(() => {
|
||||
const [range] = view.state.selection.ranges
|
||||
expect(range.head).to.eq(49)
|
||||
expect(range.anchor).to.eq(49)
|
||||
expect(range.empty).to.eq(true)
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,169 @@
|
||||
import { foldEffect, foldState } from '@codemirror/language'
|
||||
import { EditorSelection, EditorState } from '@codemirror/state'
|
||||
import { DecorationSet, EditorView } from '@codemirror/view'
|
||||
import { expect } from 'chai'
|
||||
import { duplicateSelection } from '../../../../../frontend/js/features/source-editor/commands/ranges'
|
||||
|
||||
type Position = {
|
||||
from: number
|
||||
to: number
|
||||
}
|
||||
|
||||
function folds(foldRanges: DecorationSet) {
|
||||
const ranges: Position[] = []
|
||||
foldRanges.between(Number.MIN_VALUE, Number.MAX_VALUE, (from, to) => {
|
||||
ranges.push({ from, to })
|
||||
})
|
||||
return ranges
|
||||
}
|
||||
|
||||
describe('Line duplication command', function () {
|
||||
describe('For single selections', function () {
|
||||
describe('For cursor selection', function () {
|
||||
it('Cursor selection duplicates line downwards', function () {
|
||||
const view = new EditorView({
|
||||
doc: 'line1\nline2',
|
||||
selection: EditorSelection.cursor(0),
|
||||
})
|
||||
|
||||
duplicateSelection(view)
|
||||
|
||||
expect(view.state.doc.toString()).to.equal('line1\nline1\nline2')
|
||||
|
||||
expect(view.state.selection.ranges.length).to.equal(1)
|
||||
expect(view.state.selection.ranges[0].eq(EditorSelection.cursor(0))).to
|
||||
.be.true
|
||||
})
|
||||
|
||||
it('Preserves folded ranges', function () {
|
||||
const view = new EditorView({
|
||||
doc: '\\begin{itemize}\n\t\\item test\n\\end{itemize}',
|
||||
extensions: foldState,
|
||||
})
|
||||
|
||||
view.dispatch(
|
||||
view.state.update({
|
||||
selection: EditorSelection.cursor(0),
|
||||
// Fold to \begin{itemize}...\end{itemize}
|
||||
effects: [foldEffect.of({ from: 15, to: 28 })],
|
||||
})
|
||||
)
|
||||
|
||||
duplicateSelection(view)
|
||||
|
||||
expect(folds(view.state.field(foldState))).to.deep.equal([
|
||||
{ from: 15, to: 28 },
|
||||
{ from: 57, to: 70 },
|
||||
])
|
||||
|
||||
expect(view.state.selection.ranges.length).to.equal(1)
|
||||
expect(view.state.selection.ranges[0].eq(EditorSelection.cursor(0))).to
|
||||
.be.true
|
||||
})
|
||||
})
|
||||
describe('For range selections', function () {
|
||||
it('Duplicates line with a cursor downwards', function () {
|
||||
const view = new EditorView({
|
||||
doc: 'line1\nline2',
|
||||
selection: EditorSelection.cursor(0),
|
||||
})
|
||||
|
||||
duplicateSelection(view)
|
||||
|
||||
expect(view.state.doc.toString()).to.equal('line1\nline1\nline2')
|
||||
})
|
||||
|
||||
it('Duplicates range forwards', function () {
|
||||
const view = new EditorView({
|
||||
doc: 'line1\nline2',
|
||||
selection: EditorSelection.range(0, 5),
|
||||
})
|
||||
|
||||
duplicateSelection(view)
|
||||
|
||||
expect(view.state.doc.toString()).to.equal('line1line1\nline2')
|
||||
expect(view.state.selection.ranges.length).to.equal(1)
|
||||
expect(view.state.selection.ranges[0].eq(EditorSelection.range(5, 10)))
|
||||
.to.be.true
|
||||
})
|
||||
|
||||
it('Duplicates range backwards', function () {
|
||||
const view = new EditorView({
|
||||
doc: 'line1\nline2',
|
||||
selection: EditorSelection.range(5, 0),
|
||||
})
|
||||
|
||||
duplicateSelection(view)
|
||||
|
||||
expect(view.state.doc.toString()).to.equal('line1line1\nline2')
|
||||
expect(view.state.selection.ranges.length).to.equal(1)
|
||||
expect(view.state.selection.ranges[0].eq(EditorSelection.range(5, 0)))
|
||||
.to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('For multiple selections', function () {
|
||||
it('Preserves folded ranges', function () {
|
||||
const doc =
|
||||
'\\begin{itemize}\n\t\\item line1\n\\end{itemize}\n\\begin{itemize}\n\t\\item line2\n\\end{itemize}'
|
||||
const view = new EditorView({
|
||||
doc,
|
||||
extensions: [foldState, EditorState.allowMultipleSelections.of(true)],
|
||||
})
|
||||
|
||||
view.dispatch(
|
||||
view.state.update({
|
||||
selection: EditorSelection.create([
|
||||
EditorSelection.cursor(0),
|
||||
EditorSelection.cursor(43),
|
||||
]),
|
||||
effects: [
|
||||
foldEffect.of({ from: 15, to: 29 }),
|
||||
foldEffect.of({ from: 58, to: 72 }),
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
duplicateSelection(view)
|
||||
|
||||
expect(view.state.doc.toString()).to.equal(
|
||||
'\\begin{itemize}\n\t\\item line1\n\\end{itemize}\n\\begin{itemize}\n\t\\item line1\n\\end{itemize}\n\\begin{itemize}\n\t\\item line2\n\\end{itemize}\n\\begin{itemize}\n\t\\item line2\n\\end{itemize}'
|
||||
)
|
||||
|
||||
expect(folds(view.state.field(foldState))).to.deep.equal([
|
||||
{ from: 15, to: 29 },
|
||||
{ from: 58, to: 72 },
|
||||
{ from: 101, to: 115 },
|
||||
{ from: 144, to: 158 },
|
||||
])
|
||||
|
||||
expect(view.state.selection.ranges.length).to.equal(2)
|
||||
expect(view.state.selection.ranges[0].eq(EditorSelection.cursor(0))).to.be
|
||||
.true
|
||||
expect(view.state.selection.ranges[1].eq(EditorSelection.cursor(86))).to
|
||||
.be.true
|
||||
})
|
||||
|
||||
it('Duplicates all selections', function () {
|
||||
const view = new EditorView({
|
||||
doc: 'line1\nline2',
|
||||
extensions: [EditorState.allowMultipleSelections.of(true)],
|
||||
selection: EditorSelection.create([
|
||||
EditorSelection.cursor(1),
|
||||
EditorSelection.range(7, 9),
|
||||
]),
|
||||
})
|
||||
|
||||
duplicateSelection(view)
|
||||
|
||||
expect(view.state.doc.toString()).to.equal('line1\nline1\nlinine2')
|
||||
|
||||
expect(view.state.selection.ranges.length).to.equal(2)
|
||||
expect(view.state.selection.ranges[0].eq(EditorSelection.cursor(1))).to.be
|
||||
.true
|
||||
expect(view.state.selection.ranges[1].eq(EditorSelection.range(15, 17)))
|
||||
.to.be.true
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,115 @@
|
||||
import { expect } from 'chai'
|
||||
import { EditorView, DecorationSet } from '@codemirror/view'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import { buildDecorations } from '../../../../../frontend/js/features/source-editor/extensions/line-wrapping-indentation'
|
||||
|
||||
const basicDoc = `
|
||||
\\begin{document}
|
||||
Test
|
||||
\\end{document}
|
||||
`
|
||||
|
||||
const docLongLineNoIndentation = `
|
||||
\\begin{document}
|
||||
Test
|
||||
Hello one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four
|
||||
\\end{document}
|
||||
`
|
||||
|
||||
const docLongLineWithIndentation = `
|
||||
\\begin{document}
|
||||
Test
|
||||
Hello one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four
|
||||
\\end{document}
|
||||
`
|
||||
|
||||
const docLongLineWithLotsOfIndentation = `
|
||||
\\begin{document}
|
||||
Test
|
||||
Hello one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four
|
||||
|
||||
Hello one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four
|
||||
|
||||
Hello one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four
|
||||
|
||||
Hello
|
||||
|
||||
Hello one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four
|
||||
|
||||
Hello
|
||||
\\end{document}
|
||||
`
|
||||
|
||||
describe('line-wrapping-indentation', function () {
|
||||
describe('buildDecorations', function () {
|
||||
const _buildView = (doc: string) => {
|
||||
return new EditorView({
|
||||
state: EditorState.create({
|
||||
doc,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const _toArray = (decorations: DecorationSet) => {
|
||||
const result = []
|
||||
const cursor = decorations.iter()
|
||||
while (cursor.value) {
|
||||
result.push({ from: cursor.from, to: cursor.to, value: cursor.value })
|
||||
cursor.next()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
describe('basic document', function () {
|
||||
it('should have no decorations', function () {
|
||||
const view = _buildView(basicDoc)
|
||||
|
||||
const decorations = buildDecorations(view, 24)
|
||||
expect(decorations).to.exist
|
||||
expect(decorations.size).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('document with long lines, no indentation', function () {
|
||||
it('should have no decorations', function () {
|
||||
const view = _buildView(docLongLineNoIndentation)
|
||||
|
||||
const decorations = buildDecorations(view, 24)
|
||||
expect(decorations).to.exist
|
||||
expect(decorations.size).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('document with long lines, with indentation', function () {
|
||||
it('should have a decoration', function () {
|
||||
const view = _buildView(docLongLineWithIndentation)
|
||||
|
||||
const decorations = buildDecorations(view, 24)
|
||||
expect(decorations).to.exist
|
||||
expect(decorations.size).to.equal(1)
|
||||
|
||||
const decorationItem = _toArray(decorations)[0]
|
||||
expect(decorationItem.from).to.equal(23)
|
||||
expect(decorationItem.to).to.equal(23)
|
||||
})
|
||||
})
|
||||
|
||||
describe('document with long lines, with lots of indentation', function () {
|
||||
it('should have a decoration', function () {
|
||||
const view = _buildView(docLongLineWithLotsOfIndentation)
|
||||
|
||||
const decorations = buildDecorations(view, 24)
|
||||
expect(decorations).to.exist
|
||||
expect(decorations.size).to.equal(4)
|
||||
|
||||
const decorationsArray = _toArray(decorations)
|
||||
const expectedPositions = [23, 265, 507, 758]
|
||||
|
||||
decorationsArray.forEach((item, index) => {
|
||||
expect(item.from).to.equal(expectedPositions[index])
|
||||
expect(item.to).to.equal(expectedPositions[index])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,66 @@
|
||||
import sinon from 'sinon'
|
||||
import { expect } from 'chai'
|
||||
import { EditorFacade } from '../../../../../frontend/js/features/source-editor/extensions/realtime'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
|
||||
describe('CodeMirror EditorFacade', function () {
|
||||
let state: EditorState, view: EditorView
|
||||
beforeEach(function () {
|
||||
state = EditorState.create()
|
||||
view = new EditorView({ state })
|
||||
})
|
||||
|
||||
it('should allow us to manipulate the CodeMirror document', function () {
|
||||
const editor = new EditorFacade(view)
|
||||
const text = 'basic test, nothing more'
|
||||
|
||||
editor.cmInsert(0, text)
|
||||
|
||||
expect(editor.getValue()).to.equal(text)
|
||||
|
||||
editor.cmDelete(0, 'b')
|
||||
|
||||
expect(editor.getValue()).to.equal(text.slice(1))
|
||||
})
|
||||
|
||||
it('should allow us to attach change listeners', function () {
|
||||
const editor = new EditorFacade(view)
|
||||
const listenerA = sinon.stub()
|
||||
const listenerB = sinon.stub()
|
||||
|
||||
editor.on('change', listenerA)
|
||||
editor.on('change', listenerB)
|
||||
|
||||
expect(listenerA).to.not.have.been.called
|
||||
expect(listenerB).to.not.have.been.called
|
||||
|
||||
const magicNumber = Math.random()
|
||||
editor.emit('change', magicNumber)
|
||||
|
||||
expect(listenerA).to.have.been.calledWith(magicNumber)
|
||||
expect(listenerB).to.have.been.calledWith(magicNumber)
|
||||
})
|
||||
|
||||
it('should attach to ShareJs document', function () {
|
||||
const editor = new EditorFacade(view)
|
||||
const text = 'something nice'
|
||||
const shareDoc = {
|
||||
on: sinon.stub(),
|
||||
getText: sinon.stub().returns(text),
|
||||
removeListener: sinon.stub(),
|
||||
detach_cm6: undefined,
|
||||
}
|
||||
|
||||
editor.cmInsert(0, text)
|
||||
|
||||
// @ts-ignore
|
||||
editor.attachShareJs(shareDoc)
|
||||
|
||||
expect(shareDoc.on.callCount).to.equal(2)
|
||||
expect(shareDoc.on).to.have.been.calledWith('insert')
|
||||
expect(shareDoc.on).to.have.been.calledWith('delete')
|
||||
|
||||
expect(shareDoc.detach_cm6).to.be.a('function')
|
||||
})
|
||||
})
|
@@ -0,0 +1,119 @@
|
||||
import sinon from 'sinon'
|
||||
import { fireEvent, waitFor } from '@testing-library/react'
|
||||
import { expect } from 'chai'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import {
|
||||
restoreScrollPosition,
|
||||
scrollPosition,
|
||||
} from '../../../../../frontend/js/features/source-editor/extensions/scroll-position'
|
||||
|
||||
const doc = `
|
||||
\\documentclass{article}
|
||||
\\title{Your Paper}
|
||||
\\author{You}
|
||||
\\begin{document}
|
||||
\\maketitle
|
||||
\\begin{abstract}
|
||||
Your abstract.
|
||||
\\end{abstract}
|
||||
\\section{Introduction}
|
||||
Your introduction goes here!
|
||||
\\end{document}`
|
||||
|
||||
const mockDoc = () => {
|
||||
return {
|
||||
doc_id: 'test-doc',
|
||||
}
|
||||
}
|
||||
|
||||
describe('CodeMirror scroll position extension', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(HTMLElement.prototype, 'scrollHeight').returns(800)
|
||||
sinon.stub(HTMLElement.prototype, 'scrollWidth').returns(500)
|
||||
sinon.stub(HTMLElement.prototype, 'clientHeight').returns(200)
|
||||
sinon.stub(HTMLElement.prototype, 'clientWidth').returns(500)
|
||||
|
||||
sinon
|
||||
.stub(HTMLElement.prototype, 'getBoundingClientRect')
|
||||
.returns({ top: 100, left: 0, right: 500, bottom: 200 } as DOMRect)
|
||||
|
||||
// Range.getClientRects doesn't exist yet in jsdom
|
||||
window.Range.prototype.getClientRects = sinon.stub().returns([])
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore()
|
||||
// @ts-ignore
|
||||
delete window.Range.prototype.getClientRects
|
||||
})
|
||||
|
||||
it('stores scroll position when the view is destroyed', async function () {
|
||||
const currentDoc = mockDoc()
|
||||
|
||||
sinon.stub(window.Storage.prototype, 'getItem').callsFake(key => {
|
||||
switch (key) {
|
||||
case 'doc.position.test-doc':
|
||||
return JSON.stringify({
|
||||
cursorPosition: { row: 2, column: 2 },
|
||||
firstVisibleLine: 5,
|
||||
})
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc,
|
||||
extensions: [scrollPosition({ currentDoc }, { visual: false })],
|
||||
}),
|
||||
})
|
||||
|
||||
const setItem = sinon.spy(window.Storage.prototype, 'setItem')
|
||||
fireEvent.scroll(view.scrollDOM, { target: { scrollTop: 10 } })
|
||||
|
||||
view.destroy()
|
||||
|
||||
const expected = JSON.stringify({
|
||||
cursorPosition: { row: 2, column: 2 },
|
||||
firstVisibleLine: 12,
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setItem).to.have.been.calledWith('doc.position.test-doc', expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('restores scroll position', async function () {
|
||||
const currentDoc = mockDoc()
|
||||
|
||||
const getItem = sinon
|
||||
.stub(window.Storage.prototype, 'getItem')
|
||||
.callsFake(key => {
|
||||
switch (key) {
|
||||
case 'editor.position.test-doc':
|
||||
return JSON.stringify({ firstVisibleLine: 12 })
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc,
|
||||
extensions: [scrollPosition({ currentDoc }, { visual: false })],
|
||||
}),
|
||||
})
|
||||
view.dispatch(restoreScrollPosition())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getItem).to.have.been.calledWith('doc.position.test-doc')
|
||||
})
|
||||
|
||||
// TODO: scrollTop should be a higher value but requires more mocking
|
||||
// await waitFor(() => {
|
||||
// expect(view.scrollDOM.scrollTop).to.eq(0)
|
||||
// })
|
||||
})
|
||||
})
|
@@ -0,0 +1,61 @@
|
||||
import { WordCache } from '@/features/source-editor/extensions/spelling/cache'
|
||||
import { expect } from 'chai'
|
||||
import { Word } from '@/features/source-editor/extensions/spelling/spellchecker'
|
||||
|
||||
describe('WordCache', function () {
|
||||
describe('basic operations', function () {
|
||||
let cache: WordCache, lang: string
|
||||
beforeEach(function () {
|
||||
cache = new WordCache()
|
||||
lang = 'xx'
|
||||
})
|
||||
|
||||
it('should store values in cache', function () {
|
||||
let word = 'foo'
|
||||
expect(cache.get(lang, word)).to.not.exist
|
||||
cache.set(lang, word, true)
|
||||
expect(cache.get(lang, word)).to.equal(true)
|
||||
|
||||
word = 'bar'
|
||||
expect(cache.get(lang, word)).to.not.exist
|
||||
cache.set(lang, word, false)
|
||||
expect(cache.get(lang, word)).to.equal(false)
|
||||
})
|
||||
|
||||
it('should store words in separate languages', function () {
|
||||
const word = 'foo'
|
||||
const otherLang = 'zz'
|
||||
|
||||
cache.set(lang, word, true)
|
||||
expect(cache.get(lang, word)).to.equal(true)
|
||||
expect(cache.get(otherLang, word)).to.not.exist
|
||||
|
||||
cache.set(otherLang, word, false)
|
||||
expect(cache.get(lang, word)).to.equal(true)
|
||||
expect(cache.get(otherLang, word)).to.equal(false)
|
||||
})
|
||||
|
||||
it('should check words against cache', function () {
|
||||
cache.set(lang, 'foo', false)
|
||||
cache.set(lang, 'bar', true)
|
||||
cache.set(lang, 'baz', true)
|
||||
const wordsToCheck = [
|
||||
{ text: 'foo', from: 0 },
|
||||
{ text: 'baz', from: 1 },
|
||||
{ text: 'quux', from: 2 },
|
||||
{ text: 'foo', from: 3 },
|
||||
{ text: 'zaz', from: 4 },
|
||||
] as Word[]
|
||||
const result = cache.checkWords(lang, wordsToCheck)
|
||||
expect(result).to.have.keys('knownMisspelledWords', 'unknownWords')
|
||||
expect(result.knownMisspelledWords).to.deep.equal([
|
||||
{ text: 'foo', from: 0 },
|
||||
{ text: 'foo', from: 3 },
|
||||
])
|
||||
expect(result.unknownWords).to.deep.equal([
|
||||
{ text: 'quux', from: 2 },
|
||||
{ text: 'zaz', from: 4 },
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
getWordsFromLine,
|
||||
buildSpellCheckResult,
|
||||
Word,
|
||||
} from '@/features/source-editor/extensions/spelling/spellchecker'
|
||||
import { expect } from 'chai'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { LaTeXLanguage } from '@/features/source-editor/languages/latex/latex-language'
|
||||
import { LanguageSupport } from '@codemirror/language'
|
||||
|
||||
const extensions = [new LanguageSupport(LaTeXLanguage)]
|
||||
|
||||
describe('SpellChecker', function () {
|
||||
describe('getWordsFromLine', function () {
|
||||
let lang: string
|
||||
beforeEach(function () {
|
||||
/* Note: ignore the word 'test' */
|
||||
lang = 'en'
|
||||
})
|
||||
|
||||
it('should get words from a line', function () {
|
||||
const view = new EditorView({
|
||||
doc: 'Hello test one two',
|
||||
extensions,
|
||||
})
|
||||
const line = view.state.doc.line(1)
|
||||
const words = Array.from(getWordsFromLine(view, line, lang))
|
||||
expect(words).to.deep.equal([
|
||||
{ text: 'Hello', from: 0, to: 5, lineNumber: 1, lang: 'en' },
|
||||
{ text: 'test', from: 6, to: 10, lineNumber: 1, lang: 'en' },
|
||||
{ text: 'one', from: 11, to: 14, lineNumber: 1, lang: 'en' },
|
||||
{ text: 'two', from: 15, to: 18, lineNumber: 1, lang: 'en' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should get no words from an empty line', function () {
|
||||
const view = new EditorView({
|
||||
doc: ' ',
|
||||
extensions,
|
||||
})
|
||||
const line = view.state.doc.line(1)
|
||||
const words = Array.from(getWordsFromLine(view, line, lang))
|
||||
expect(words).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('should ignore content of some commands in the text', function () {
|
||||
const view = new EditorView({
|
||||
doc: '\\usepackage[foo]{ bar } seven eight',
|
||||
extensions,
|
||||
})
|
||||
const line = view.state.doc.line(1)
|
||||
const words = Array.from(getWordsFromLine(view, line, lang))
|
||||
expect(words).to.deep.equal([
|
||||
{ text: 'seven', from: 24, to: 29, lineNumber: 1, lang: 'en' },
|
||||
{ text: 'eight', from: 30, to: 35, lineNumber: 1, lang: 'en' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should ignore command names in the text', function () {
|
||||
const view = new EditorView({
|
||||
doc: '\\foo nine \\bar ten \\baz[]{}',
|
||||
extensions,
|
||||
})
|
||||
const line = view.state.doc.line(1)
|
||||
const words = Array.from(getWordsFromLine(view, line, lang))
|
||||
expect(words).to.deep.equal([
|
||||
{ text: 'nine', from: 5, to: 9, lineNumber: 1, lang: 'en' },
|
||||
{ text: 'ten', from: 15, to: 18, lineNumber: 1, lang: 'en' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildSpellCheckResult', function () {
|
||||
it('should build an empty result', function () {
|
||||
const knownMisspelledWords: Word[] = []
|
||||
const unknownWords: Word[] = []
|
||||
const misspellings: { index: number }[] = []
|
||||
const result = buildSpellCheckResult(
|
||||
knownMisspelledWords,
|
||||
unknownWords,
|
||||
misspellings
|
||||
)
|
||||
expect(result).to.deep.equal({
|
||||
cacheAdditions: [],
|
||||
misspelledWords: [],
|
||||
})
|
||||
})
|
||||
it('should build a realistic result', function () {
|
||||
const _makeWord = (text: string) => {
|
||||
return new Word({
|
||||
text,
|
||||
from: 0,
|
||||
to: 0,
|
||||
lineNumber: 0,
|
||||
lang: 'xx',
|
||||
})
|
||||
}
|
||||
// We know this word is misspelled
|
||||
const knownMisspelledWords = [_makeWord('fff')]
|
||||
// These words we didn't know
|
||||
const unknownWords = [
|
||||
_makeWord('aaa'),
|
||||
_makeWord('bbb'),
|
||||
_makeWord('ccc'),
|
||||
_makeWord('ddd'),
|
||||
]
|
||||
// These are the suggestions we got back from the backend
|
||||
const misspellings = [{ index: 1 }, { index: 3 }]
|
||||
// Build the result structure
|
||||
const result = buildSpellCheckResult(
|
||||
knownMisspelledWords,
|
||||
unknownWords,
|
||||
misspellings
|
||||
)
|
||||
expect(result).to.have.keys('cacheAdditions', 'misspelledWords')
|
||||
// Check cache additions
|
||||
expect(result.cacheAdditions.map(([k, v]) => [k.text, v])).to.deep.equal([
|
||||
// Put these in cache as known misspellings
|
||||
['bbb', false],
|
||||
['ddd', false],
|
||||
// Put these in cache as known-correct
|
||||
['aaa', true],
|
||||
['ccc', true],
|
||||
])
|
||||
// Check misspellings
|
||||
expect(result.misspelledWords.map(w => w.text)).to.deep.equal([
|
||||
// Words in the payload that we now know were misspelled
|
||||
'bbb',
|
||||
'ddd',
|
||||
// Word we already knew was misspelled, preserved here
|
||||
'fff',
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user