substance
Version:
Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing system. It is developed to power our online editing platform [Substance](http://substance.io).
1,461 lines (1,385 loc) • 89.9 kB
JavaScript
/* eslint-disable no-use-before-define */
import { test } from 'substance-test'
import { EditingInterface, forEach, documentHelpers } from 'substance'
import setupEditor from './shared/setupEditor'
import headersAndParagraphs from './fixture/headersAndParagraphs'
import {
_h1,
_p1, P1_TEXT,
_p2, P2_TEXT,
_s1, _empty, _il1,
_block1, _block2,
_in1, IN1_TITLE,
_l1, _l11, _l12, _l13, _l1Empty,
_li1plus, _li2plus,
LI1_TEXT, LI2_TEXT, LI3_TEXT,
_l2, _l21, _l22,
LI22_TEXT, LI21_TEXT
} from './fixture/samples'
// TODO: consolidate specification and 'category labels' of tests
// TODO: we should enable t.sandbox for all tests so that this implementation is
// tested on all platforms
test('Editing: IT1: Inserting text with cursor in the middle of a TextProperty', (t) => {
const { editorSession, doc } = setupEditor(t, _p1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 3,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('xxx')
}, { action: 'type' })
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
t.equal(p1.getText(), P1_TEXT.slice(0, 3) + 'xxx' + P1_TEXT.slice(3), 'Text should have been inserted correctly.')
t.equal(sel.start.offset, 6, 'Cursor should be after inserted text')
t.end()
})
test('Editing: IT2: Inserting text with cursor within TextProperty inside annotation', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _s1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 4,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('xxx')
}, { action: 'type' })
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
const s1 = doc.get('s1')
t.equal(p1.getText(), P1_TEXT.slice(0, 4) + 'xxx' + P1_TEXT.slice(4), 'Text should have been inserted correctly.')
t.equal(s1.end.offset, 8, 'Annotation should have been expanded.')
t.equal(sel.start.offset, 7, 'Cursor should be after inserted text')
t.end()
})
test('Editing: IT3: Inserting text with cursor within TextProperty at the start of an annotation', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _s1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 3,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('xxx')
}, { action: 'type' })
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
const s1 = doc.get('s1')
t.equal(p1.getText(), P1_TEXT.slice(0, 3) + 'xxx' + P1_TEXT.slice(3), 'Text should have been inserted correctly.')
t.equal(s1.start.offset, 6, 'Annotation should have been moved.')
t.equal(s1.end.offset, 8, 'Annotation should have been moved.')
t.equal(sel.start.offset, 6, 'Cursor should be after inserted text')
t.end()
})
test('Editing: IT4: Inserting text with cursor within TextProperty at the end of an annotation', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _s1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 5,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('xxx')
}, { action: 'type' })
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
const s1 = doc.get('s1')
t.equal(p1.getText(), P1_TEXT.slice(0, 5) + 'xxx' + P1_TEXT.slice(5), 'Text should have been inserted correctly.')
t.equal(s1.start.offset, 3, 'Annotation should have been moved.')
t.equal(s1.end.offset, 8, 'Annotation should have been moved.')
t.equal(sel.start.offset, 8, 'Cursor should be after inserted text')
t.end()
})
test('Editing: IT5: Inserting text with range within TextProperty', (t) => {
const { editorSession, doc } = setupEditor(t, _p1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 2,
endOffset: 5,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('xxx')
}, { action: 'type' })
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
t.equal(p1.getText(), P1_TEXT.slice(0, 2) + 'xxx' + P1_TEXT.slice(5), 'Text should have been inserted correctly.')
t.equal(sel.start.offset, 5, 'Cursor should be after inserted text')
t.ok(sel.isCollapsed(), '... collapsed')
t.end()
})
test('Editing: IT6: Inserting text with range within TextProperty overlapping an annotion aligned at the left side', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _s1)
doc.set(['s1', 'end', 'offset'], 6)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 3,
endOffset: 5,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('xxx')
}, { action: 'type' })
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
const s1 = doc.get('s1')
t.equal(p1.getText(), P1_TEXT.slice(0, 3) + 'xxx' + P1_TEXT.slice(5), 'Text should have been inserted correctly.')
t.equal(s1.start.offset, 3, 'Annotation should have been moved.')
t.equal(s1.end.offset, 7, 'Annotation should have been moved.')
t.equal(sel.start.offset, 6, 'Cursor should be after inserted text')
t.ok(sel.isCollapsed(), '... collapsed')
t.end()
})
test('Editing: IT7: Inserting text with range within TextProperty inside an annotion', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _s1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 3,
endOffset: 5,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('xxx')
}, { action: 'type' })
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
const s1 = doc.get('s1')
t.equal(p1.getText(), P1_TEXT.slice(0, 3) + 'xxx' + P1_TEXT.slice(5), 'Text should have been inserted correctly.')
t.equal(s1.start.offset, 3, 'Annotation should have been moved.')
t.equal(s1.end.offset, 6, 'Annotation should have been moved.')
t.equal(sel.start.offset, 6, 'Cursor should be after inserted text')
t.ok(sel.isCollapsed(), '... collapsed')
t.end()
})
test('Editing: IT8: Inserting text with range within TextProperty starting inside an annotion', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _s1)
doc.set(['s1', 'end', 'offset'], 6)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 4,
endOffset: 6,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('xxx')
}, { action: 'type' })
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
const s1 = doc.get('s1')
t.equal(p1.getText(), P1_TEXT.slice(0, 4) + 'xxx' + P1_TEXT.slice(6), 'Text should have been inserted correctly.')
t.equal(s1.start.offset, 3, 'Annotation should have been moved.')
t.equal(s1.end.offset, 7, 'Annotation should have been moved.')
t.equal(sel.start.offset, 7, 'Cursor should be after inserted text')
t.ok(sel.isCollapsed(), '... collapsed')
t.end()
})
test('Editing: IT9: Inserting text after an InlineNode node', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _il1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 4,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('Y')
}, { action: 'type' })
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
const il1 = doc.get('il1')
t.equal(p1.getText(), P1_TEXT.slice(0, 3) + '\uFEFF' + 'Y' + P1_TEXT.slice(3), 'Text should have been inserted correctly.') // eslint-disable-line no-useless-concat
t.equal(sel.start.offset, 5, 'Cursor should be after inserted character')
t.deepEqual([il1.start.offset, il1.end.offset], [3, 4], 'InlineNode should have correct dimensions')
t.end()
})
test('Editing: IT10: Typing over an InlineNode node', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _il1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 3,
endOffset: 4,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('Y')
}, { action: 'type' })
const p1 = doc.get('p1')
const il1 = doc.get('il1')
t.nil(il1, 'InlineNode should have been deleted.')
t.equal(p1.getText(), P1_TEXT.slice(0, 3) + 'Y' + P1_TEXT.slice(3), 'Text should have been inserted correctly.') // eslint-disable-line no-useless-concat
t.end()
})
test('Editing: IT11: Typing over a selected IsolatedNode', (t) => {
const { editorSession, doc } = setupEditor(t, _in1)
editorSession.setSelection({
type: 'node',
nodeId: 'in1',
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('Y')
}, { action: 'type' })
const body = doc.get('body')
t.isNil(doc.get('in1'), 'IsolatedNode should have been deleted.')
t.equal(body.getLength(), 1, 'There should be one node.')
t.equal(body.getNodeAt(0).getText(), 'Y', '.. containing the typed text.')
t.end()
})
test('Editing: IT12: Typing before an IsolatedNode', (t) => {
const { editorSession, doc } = setupEditor(t, _in1)
editorSession.setSelection({
type: 'node',
nodeId: 'in1',
mode: 'before',
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('Y')
}, { action: 'type' })
const body = doc.get('body')
t.equal(body.getLength(), 2, 'There should be two nodes.')
t.equal(body.getNodeAt(0).getText(), 'Y', '.. first one containing the typed text.')
t.end()
})
test('Editing: IT13: Typing after an IsolatedNode', (t) => {
const { editorSession, doc } = setupEditor(t, _in1)
editorSession.setSelection({
type: 'node',
nodeId: 'in1',
mode: 'after',
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('Y')
}, { action: 'type' })
const body = doc.get('body')
t.equal(body.getLength(), 2, 'There should be two nodes.')
t.equal(body.getNodeAt(1).getText(), 'Y', '.. second one containing the typed text.')
t.end()
})
test('Editing: IT14: Typing over a ContainerSelection', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _p2)
const p1 = doc.get('p1')
const p2 = doc.get('p2')
editorSession.setSelection({
type: 'container',
startPath: p1.getPath(),
startOffset: 3,
endPath: p2.getPath(),
endOffset: 4,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('Y')
}, { action: 'type' })
const body = doc.get('body')
t.deepEqual(body.nodes, ['p1'], 'Only the first paragraph should be left.')
t.equal(p1.getText(), P1_TEXT.slice(0, 3) + 'Y' + P2_TEXT.slice(4), 'The content should have been merged correctly')
t.end()
})
test('Editing: IT15: Inserting text into a ListItem', (t) => {
const { editorSession, doc } = setupEditor(t, _l1, _l11, _l12)
editorSession.setSelection({
type: 'property',
path: ['l1-1', 'content'],
startOffset: 3,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertText('xxx')
}, { action: 'type' })
const sel = editorSession.getSelection()
const li = doc.get('l1-1')
t.equal(li.getText(), LI1_TEXT.slice(0, 3) + 'xxx' + LI1_TEXT.slice(3), 'Text should have been inserted correctly.')
t.equal(sel.start.offset, 6, 'Cursor should be after inserted text')
t.end()
})
test('Editing: IT16: Inserting text in an IsolatedNode', (t) => {
const { doc, editorSession } = setupEditor(t, _p1, _in1, _p2)
editorSession.setSelection({
type: 'property',
path: ['in1', 'title'],
startOffset: 3
})
editorSession.transaction((tx) => {
tx.insertText('xxx')
})
const in1 = doc.get('in1')
t.equal(in1.title, IN1_TITLE.slice(0, 3) + 'xxx' + IN1_TITLE.slice(3), 'Text should be inserted into field')
t.end()
})
test('Editing: II1: Inserting InlineNode node into a TextProperty', (t) => {
const { editorSession, doc } = setupEditor(t, _p1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 3,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertInlineNode({
type: 'test-inline-node',
id: 'il1',
content: 'X'
})
})
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
const il1 = doc.get('il1')
t.equal(p1.getText(), P1_TEXT.slice(0, 3) + '\uFEFF' + P1_TEXT.slice(3), 'Text should have been inserted correctly.')
t.equal(il1.start.offset, 3, 'Annotation should have been moved.')
t.equal(il1.end.offset, 4, 'Annotation should have been moved.')
t.equal(sel.start.offset, 4, 'Cursor should be after inserted inline node')
t.end()
})
test('Editing: IB2: Inserting BlockNode using cursor at start of a TextNode', (t) => {
const { editorSession, doc } = setupEditor(t, _p1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 0,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertBlockNode({
type: 'test-block',
id: 'ib1'
})
})
const sel = editorSession.getSelection()
const body = doc.get('body')
const p1 = doc.get('p1')
t.equal(body.nodes[0], 'ib1', 'First node should be inserted block node.')
t.equal(body.nodes[1], 'p1', 'Second node should be inserted block node.')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, p1.getPath(), '... on paragraph')
t.equal(sel.start.offset, 0, '... first position')
t.end()
})
test('Editing: IB3: Inserting an existing BlockNode', (t) => {
const { editorSession, doc } = setupEditor(t, _p1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 0,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.create({
type: 'test-block',
id: 'ib1'
})
})
const ib1 = doc.get('ib1')
editorSession.transaction((tx) => {
tx.insertBlockNode(ib1)
})
const body = doc.get('body')
t.ok(body.getNodeAt(0) === ib1, 'First node should be the very block node instance created before.')
t.end()
})
test('Editing: IB4: Inserting a BlockNode when an IsolatedNode is selected', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _in1, _p2)
editorSession.setSelection({
type: 'node',
nodeId: 'in1',
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertBlockNode({
type: 'test-block',
id: 'ib1'
})
})
const body = doc.get('body')
t.equal(body.length, 3, 'There should be three nodes.')
t.equal(body.nodes[1], 'ib1', 'The second one should be the inserted block node.')
t.isNil(doc.get('in1'), 'The IsolatedNode should have been deleted.')
t.end()
})
test('Editing: IB5: Inserting a BlockNode before an IsolatedNode', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _in1, _p2)
editorSession.setSelection({
type: 'node',
nodeId: 'in1',
mode: 'before',
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertBlockNode({
type: 'test-block',
id: 'ib1'
})
})
const body = doc.get('body')
t.deepEqual(body.nodes, ['p1', 'ib1', 'in1', 'p2'], 'The body nodes should be in proper order.')
t.end()
})
test('Editing: IB6: Inserting a BlockNode after an IsolatedNode', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _in1, _p2)
editorSession.setSelection({
type: 'node',
nodeId: 'in1',
mode: 'after',
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertBlockNode({
type: 'test-block',
id: 'ib1'
})
})
const body = doc.get('body')
t.deepEqual(body.nodes, ['p1', 'in1', 'ib1', 'p2'], 'The body nodes should be in proper order.')
t.end()
})
test('Editing: IB7: Inserting a BlockNode with an expanded PropertySelection', (t) => {
const { editorSession, doc } = setupEditor(t, _p1)
const p1 = doc.get('p1')
editorSession.setSelection({
type: 'property',
path: p1.getPath(),
startOffset: 3,
endOffset: 5,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertBlockNode({
type: 'test-block',
id: 'ib1'
})
})
const body = doc.get('body')
t.equal(body.getLength(), 3, 'There should be 3 nodes.')
t.equal(body.nodes[1], 'ib1', 'The second should be the inserted block node.')
t.equal(p1.getText(), P1_TEXT.slice(0, 3), 'The paragraph should have been truncated.')
t.equal(body.getNodeAt(2).getText(), P1_TEXT.slice(5), '.. and the tail stored in a new paragraph.')
t.end()
})
test('Editing: IB8: Inserting a BlockNode into an empty paragraph', (t) => {
const { editorSession, doc } = setupEditor(t, _empty)
const empty = doc.get('empty')
editorSession.setSelection({
type: 'property',
path: empty.getPath(),
startOffset: 0,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertBlockNode({
type: 'test-block',
id: 'ib1'
})
})
const body = doc.get('body')
t.deepEqual(body.nodes, ['ib1'], 'There should only be the inserted block node.')
t.nil(doc.get('empty'), 'The empty paragraph should have been deleted.')
t.end()
})
test('Editing: IB9: Inserting a BlockNode after a text node', (t) => {
const { editorSession, doc } = setupEditor(t, _p1)
const p1 = doc.get('p1')
editorSession.setSelection({
type: 'property',
path: p1.getPath(),
startOffset: p1.getLength(),
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertBlockNode({
type: 'test-block',
id: 'ib1'
})
})
const body = doc.get('body')
t.deepEqual(body.nodes, ['p1', 'ib1'], 'The block node should have been inserted after the paragraph.')
t.end()
})
test('Editing: IB10: Inserting a BlockNode with a collapsed ContainerSelection', (t) => {
const { editorSession, doc } = setupEditor(t, _p1)
const p1 = doc.get('p1')
editorSession.setSelection({
type: 'container',
startPath: p1.getPath(),
startOffset: 3,
endPath: p1.getPath(),
endOffset: 3,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertBlockNode({
type: 'test-block',
id: 'ib1'
})
})
const body = doc.get('body')
t.equal(body.getLength(), 3, 'There should be three nodes.')
t.equal(body.nodes[1], 'ib1', '.. the second one being the inserted block node.')
t.end()
})
test('Editing: IB11: Inserting a BlockNode with a ContainerSelection', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _p2)
const p1 = doc.get('p1')
const p2 = doc.get('p2')
editorSession.setSelection({
type: 'container',
startPath: p1.getPath(),
startOffset: 3,
endPath: p2.getPath(),
endOffset: 4,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.insertBlockNode({
type: 'test-block',
id: 'ib1'
})
})
const body = doc.get('body')
t.deepEqual(body.nodes, ['p1', 'ib1', 'p2'], 'There should be three nodes.')
t.equal(p1.getText(), P1_TEXT.slice(0, 3), 'The first paragraph should have been truncated correctly')
t.equal(p2.getText(), P2_TEXT.slice(4), 'The second paragraph should have been truncated correctly')
t.end()
})
test('Editing: DEL0: Deleting without selection', (t) => {
const { editorSession } = setupEditor(t, _p1, _p2)
editorSession.setSelection(null)
t.doesNotThrow(() => {
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
}, 'Should not throw')
t.end()
})
test('Editing: DEL1: Deleting a property selection', (t) => {
const { editorSession, doc } = setupEditor(t, _p1)
const p1 = doc.get('p1')
editorSession.setSelection({
type: 'property',
path: p1.getPath(),
startOffset: 0,
endOffset: 3
})
editorSession.transaction((tx) => {
tx.deleteSelection()
})
t.equal(p1.getText(), P1_TEXT.slice(3), 'Parargaph should be truncated')
t.end()
})
test('Editing: DEL2: Deleting using DELETE with cursor at the end of a TextNode at the end of a Container', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _p2)
editorSession.setSelection({
type: 'property',
path: ['p2', 'content'],
startOffset: P2_TEXT.length,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
// nothing should have happened
const sel = editorSession.getSelection()
const body = doc.get('body')
t.equal(body.nodes.length, 2, 'There should still be 2 nodes in body')
const p2 = doc.get('p2')
t.equal(p2.getText(), P2_TEXT, 'p2 should still have same content.')
t.equal(sel.start.offset, P2_TEXT.length, 'Cursor should still be at the same position')
t.end()
})
test('Editing: DEL3: Deleting using DELETE with cursor in the middle of a TextProperty', (t) => {
const { editorSession, doc } = setupEditor(t, _p1)
editorSession.transaction((tx) => {
tx.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 3
})
tx.deleteCharacter('right')
})
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
t.equal(p1.getText(), P1_TEXT.slice(0, 3) + P1_TEXT.slice(4), 'One character should have been deleted')
t.equal(sel.start.offset, 3, 'Cursor should be at the same position')
t.end()
})
test('Editing: DEL4: Deleting using DELETE with cursor inside an empty TextNode and TextNode as successor', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _empty, _p2)
editorSession.setSelection({
type: 'property',
path: ['empty', 'content'],
startOffset: 0,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
const sel = editorSession.getSelection()
const body = doc.get('body')
const p2 = doc.get('p2')
t.equal(body.nodes.length, 2, 'There should be only 2 nodes left.')
t.equal(p2.getText(), P2_TEXT, 'p2 should not be affected')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, p2.getPath(), '... on p2')
t.equal(sel.start.offset, 0, '... at first position')
t.end()
})
test('Editing: DEL5: Deleting using DELETE with cursor inside an empty TextNode and IsolatedNode as successor', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _empty, _block1)
editorSession.setSelection({
type: 'property',
path: ['empty', 'content'],
startOffset: 0,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
const sel = editorSession.getSelection()
const body = doc.get('body')
t.equal(body.nodes.length, 2, 'There should be only 2 nodes left.')
t.ok(sel.isNodeSelection(), 'Selection should be a NodeSelection')
t.equal(sel.getNodeId(), 'block1', '... block1')
t.end()
})
test('Editing: DEL6: Deleting using DELETE with cursor inside an empty TextNode and List as successor', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _empty, _l1, _l11, _l12)
editorSession.setSelection({
type: 'property',
path: ['empty', 'content'],
startOffset: 0,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
const sel = editorSession.getSelection()
const body = doc.get('body')
t.equal(body.nodes.length, 2, 'There should be only 2 nodes left.')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, ['l1-1', 'content'], '... on first list item')
t.equal(sel.start.offset, 0, '... at first position')
t.end()
})
test('Editing: DEL7: Deleting using DELETE with cursor at the end of a non-empty TextNode and TextNode as successor', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _p2)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: P1_TEXT.length,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
const sel = editorSession.getSelection()
const body = doc.get('body')
const p1 = doc.get('p1')
t.equal(body.nodes.length, 1, 'There should be only one node left.')
t.ok(sel.isCollapsed(), 'Selection should be a collapsed')
t.deepEqual(sel.start.path, p1.getPath(), '... on p1')
t.equal(sel.start.offset, P1_TEXT.length, '... at the same position as before')
t.end()
})
test('Editing: DEL8: Deleting using DELETE with cursor at the end of a non-empty TextNode and IsolatedNode as successor', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _block1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: P1_TEXT.length,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
// NOTE: if there is no merge possible, nothing should happen
// only the selection should be updated
const sel = editorSession.getSelection()
const body = doc.get('body')
t.equal(body.nodes.length, 2, 'There should still be 2 nodes left.')
t.ok(sel.isNodeSelection(), 'Selection should be a NodeSelection')
t.equal(sel.getNodeId(), 'block1', '... in block1')
t.ok(sel.isFull(), '... selecting the whole node')
t.end()
})
test('Editing: DEL9: Deleting using DELETE with cursor after an IsolatedNode and a TextNode as successor', (t) => {
const { editorSession, doc } = setupEditor(t, _block1, _p1)
editorSession.setSelection({
type: 'node',
nodeId: 'block1',
mode: 'after',
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
const p1 = doc.get('p1')
t.equal(p1.getText(), P1_TEXT.slice(1), 'First character of paragraph should have been deleted.')
_checkSelection(t, editorSession.getSelection(), {
type: 'property',
path: p1.getPath(),
startOffset: 0
})
t.end()
})
test('Editing: DEL10: Deleting using DELETE with cursor after an IsolatedNode and an IsolatedNode as successor', (t) => {
const { editorSession } = setupEditor(t, _block1, _block2)
editorSession.setSelection({
type: 'node',
nodeId: 'block1',
mode: 'after',
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
_checkSelection(t, editorSession.getSelection(), {
type: 'node',
nodeId: 'block2',
mode: 'full'
})
t.end()
})
test('Editing: DEL11: Deleting using BACKSPACE with cursor in the middle of a TextProperty', (t) => {
const { editorSession, doc } = setupEditor(t, _p1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 4,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('left')
})
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
t.equal(p1.getText(), P1_TEXT.slice(0, 3) + P1_TEXT.slice(4), 'one character should have been deleted')
t.equal(sel.start.offset, 3, 'Cursor should have shifted by one character')
t.end()
})
test('Editing: DEL12: Deleting using BACKSPACE with cursor inside an empty TextNode and TextNode as predecessor', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _empty, _p2)
editorSession.setSelection({
type: 'property',
path: ['empty', 'content'],
startOffset: 0,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('left')
})
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
t.isNil(doc.get('empty'), 'empty node should have been deleted')
t.equal(p1.getText(), P1_TEXT, 'p1 should be untouched')
t.deepEqual(sel.start.path, p1.getPath(), 'Cursor should be in p1')
t.equal(sel.start.offset, P1_TEXT.length, '... at last position')
t.end()
})
test('Editing: DEL13: Deleting using BACKSPACE with cursor inside an empty TextNode and IsolatedNode as predecessor', (t) => {
const { editorSession, doc } = setupEditor(t, _block1, _empty, _p2)
editorSession.setSelection({
type: 'property',
path: ['empty', 'content'],
startOffset: 0,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('left')
})
const sel = editorSession.getSelection()
t.isNil(doc.get('empty'), 'empty node should have been deleted')
t.ok(sel.isNodeSelection(), 'Selection should be a node selection')
t.equal(sel.getNodeId(), 'block1', '... on block1')
t.end()
})
test('Editing: DEL14: Deleting using BACKSPACE with cursor at the start of a non-empty TextNode and TextNode as predecessor', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _p2)
editorSession.setSelection({
type: 'property',
path: ['p2', 'content'],
startOffset: 0,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('left')
})
const sel = editorSession.getSelection()
const p1 = doc.get('p1')
t.isNil(doc.get('p2'), 'p2 should have been deleted')
t.equal(p1.getText(), P1_TEXT + P2_TEXT, 'Text should have been merged')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, p1.getPath(), '... on p1')
t.ok(sel.start.offset, P1_TEXT.length, '... cursor should after the original text of p1')
t.end()
})
test('Editing: DEL15: Deleting using BACKSPACE with cursor at the start of a non-empty TextNode and IsolatedNode as predecessor', (t) => {
const { editorSession, doc } = setupEditor(t, _block1, _p2)
editorSession.setSelection({
type: 'property',
path: ['p2', 'content'],
startOffset: 0,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('left')
})
const sel = editorSession.getSelection()
const body = doc.get('body')
t.equal(body.getLength(), 2, 'There should still be two nodes')
t.ok(sel.isNodeSelection(), 'Selection should be a NodeSelection')
t.equal(sel.getNodeId(), 'block1', '... on block1')
t.ok(sel.isFull(), '... selecting the full node')
t.end()
})
test('Editing: DEL16: Deleting using BACKSPACE with cursor after IsolatedNode', (t) => {
const { editorSession, doc } = setupEditor(t, _block1, _p2)
editorSession.setSelection({
type: 'node',
mode: 'after',
nodeId: 'block1',
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('left')
})
const sel = editorSession.getSelection()
const body = doc.get('body')
t.isNil(doc.get('block1'), 'IsolatedNode should have been deleted')
t.equal(body.getLength(), 2, 'There should still be two nodes')
const pnew = body.getNodeAt(0)
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, pnew.getPath(), '... on new paragraph')
t.equal(sel.start.offset, 0, '... at first position')
t.end()
})
test('Editing: DEL17: Deleting using BACKSPACE with cursor before IsolatedNode with TextNode as predecessor', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _block2)
editorSession.setSelection({
type: 'node',
mode: 'before',
nodeId: 'block2',
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('left')
})
const sel = editorSession.getSelection()
const body = doc.get('body')
const p1 = doc.get('p1')
t.equal(body.getLength(), 2, 'There should still be two nodes')
t.equal(p1.getText(), P1_TEXT.slice(0, -1), 'Last character of p1 should have been deleted')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, p1.getPath(), '... on p1')
t.equal(sel.start.offset, P1_TEXT.length - 1, '... at last position')
t.end()
})
test('Editing: DEL18: Deleting using BACKSPACE with cursor before IsolatedNode and IsolatedNode as predecessor', (t) => {
const { editorSession, doc } = setupEditor(t, _block1, _block2)
editorSession.setSelection({
type: 'node',
mode: 'before',
nodeId: 'block2',
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('left')
})
const sel = editorSession.getSelection()
const body = doc.get('body')
t.equal(body.getLength(), 2, 'There should still be two nodes')
t.ok(sel.isNodeSelection(), 'Selection should be a NodeSelection')
t.equal(sel.getNodeId(), 'block1', '... on block1')
t.ok(sel.isFull(), '... selection the entire node')
t.end()
})
test('Editing: DEL19: Deleting an entirely selected IsolatedNode', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _block1, _p2)
editorSession.setSelection({
type: 'node',
nodeId: 'block1',
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteSelection()
})
const sel = editorSession.getSelection()
const body = doc.get('body')
t.isNil(doc.get('block1'), 'IsolatedNode should have been deleted')
const pnew = body.getNodeAt(1)
t.equal(body.getLength(), 3, 'There should be 3 nodes')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, pnew.getPath(), '... on new paragraph')
t.equal(sel.start.offset, 0, '... at first position')
t.end()
})
test('Editing: DEL20: Deleting a range starting before a TextNode and ending after a TextNode', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _block1, _block2, _p2)
const p1 = doc.get('p1')
const p2 = doc.get('p2')
editorSession.setSelection({
type: 'container',
startPath: p1.getPath(),
startOffset: 0,
endPath: p2.getPath(),
endOffset: p2.getLength(),
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteSelection()
})
const sel = editorSession.getSelection()
const body = doc.get('body')
t.equal(body.nodes.length, 1, 'There should be only one node left')
const first = body.getNodeAt(0)
t.ok(first.isText(), '... which is a TextNode')
t.ok(first.isEmpty(), '... which is empty')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, first.getPath(), '... on that TextNode')
t.equal(sel.start.offset, 0, '... at first position')
t.end()
})
test('Editing: DEL21: Deleting a range starting in the middle of a TextNode and ending after a TextNode', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _block1, _block2, _p2)
const p1 = doc.get('p1')
const p2 = doc.get('p2')
editorSession.setSelection({
type: 'container',
startPath: p1.getPath(),
startOffset: 3,
endPath: p2.getPath(),
endOffset: p2.getLength(),
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteSelection()
})
const sel = editorSession.getSelection()
const body = doc.get('body')
t.equal(body.nodes.length, 1, 'There should be only one node left')
const first = body.getNodeAt(0)
t.ok(first.isText(), '... which is a TextNode')
t.equal(first.getText(), P1_TEXT.slice(0, 3), '... with truncated content')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, first.getPath(), '... on that TextNode')
t.equal(sel.start.offset, 3, '... at last position')
t.end()
})
test('Editing: DEL22: Deleting a range starting before a TextNode and ending in the middle of a TextNode', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _block1, _block2, _p2)
const p1 = doc.get('p1')
const p2 = doc.get('p2')
editorSession.setSelection({
type: 'container',
startPath: p1.getPath(),
startOffset: 0,
endPath: p2.getPath(),
endOffset: 3,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteSelection()
})
const sel = editorSession.getSelection()
const body = doc.get('body')
t.equal(body.nodes.length, 1, 'There should be only one node left')
const first = body.getNodeAt(0)
t.ok(first.isText(), '... which is a TextNode')
t.equal(first.getText(), P2_TEXT.slice(3), '... with sliced content')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, first.getPath(), '... on that TextNode')
t.equal(sel.start.offset, 0, '... at first position')
t.end()
})
test('Editing: DEL23: Deleting a range starting in the middle of a TextNode and ending in the middle of a TextNode', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _block1, _block2, _p2)
const p1 = doc.get('p1')
const p2 = doc.get('p2')
editorSession.setSelection({
type: 'container',
startPath: p1.getPath(),
startOffset: 3,
endPath: p2.getPath(),
endOffset: 3,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteSelection()
})
const sel = editorSession.getSelection()
const body = doc.get('body')
t.equal(body.nodes.length, 1, 'There should be only one node left')
const first = body.getNodeAt(0)
t.ok(first.isText(), '... which is a TextNode')
t.equal(first.getText(), P1_TEXT.slice(0, 3) + P2_TEXT.slice(3), '... with merged content')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, first.getPath(), '... on that TextNode')
t.equal(sel.start.offset, 3, '... at correct position')
t.end()
})
test('Editing: DEL24: Deleting a range starting in the middle of a TextNode and ending in the middle of a ListItem', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _block1, _block2, _l1, _l11, _l12)
const p1 = doc.get('p1')
const l1 = doc.get('l1')
editorSession.setSelection({
type: 'container',
startPath: p1.getPath(),
startOffset: 3,
endPath: ['l1-1', 'content'],
endOffset: 3,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteSelection()
})
const sel = editorSession.getSelection()
const body = doc.get('body')
t.equal(body.nodes.length, 2, 'There should be 2 nodes left')
t.equal(p1.getText(), P1_TEXT.slice(0, 3) + LI1_TEXT.slice(3), '... with merged content')
t.equal(l1.items.length, 1, 'The list should have only 1 item left')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, p1.getPath(), '... on p1')
t.equal(sel.start.offset, 3, '... at correct position')
t.end()
})
test('Editing: DEL25: Deleting a range starting in the middle of a ListItem and ending in the middle of a TextNode', (t) => {
const { editorSession, doc } = setupEditor(t, _l1, _l11, _l12, _block1, _block2, _p1)
const p1 = doc.get('p1')
const l1 = doc.get('l1')
editorSession.setSelection({
type: 'container',
startPath: ['l1-2', 'content'],
startOffset: 3,
endPath: p1.getPath(),
endOffset: 3,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteSelection()
})
const sel = editorSession.getSelection()
const body = doc.get('body')
t.equal(body.nodes.length, 1, 'There should be 1 node left')
t.equal(l1.items.length, 2, 'The list should have 2 items')
const li2 = l1.getItemAt(1)
t.equal(li2.getText(), LI2_TEXT.slice(0, 3) + P1_TEXT.slice(3), 'The second item should container merged content')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, li2.getPath(), '... on second list item')
t.equal(sel.start.offset, 3, '... at correct position')
t.end()
})
test('Editing: DEL26: Deleting a range within a ListItem', (t) => {
const { editorSession, doc } = setupEditor(t, _l1, _l11, _l12)
const l1 = doc.get('l1')
editorSession.setSelection({
type: 'container',
startPath: ['l1-2', 'content'],
startOffset: 3,
endPath: ['l1-2', 'content'],
endOffset: 6,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteSelection()
})
const sel = editorSession.getSelection()
t.equal(l1.items.length, 2, 'The list should have 2 items')
const li2 = l1.getItemAt(1)
t.equal(li2.getText(), LI2_TEXT.slice(0, 3) + LI2_TEXT.slice(6), 'The second item should be changed')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, li2.getPath(), '... on second list item')
t.equal(sel.start.offset, 3, '... at correct position')
t.end()
})
test('Editing: DEL27: Deleting a range across two ListItems within the same List', (t) => {
const { editorSession, doc } = setupEditor(t, _l1, _l11, _l12)
const l1 = doc.get('l1')
editorSession.setSelection({
type: 'container',
startPath: ['l1-1', 'content'],
startOffset: 3,
endPath: ['l1-2', 'content'],
endOffset: 3,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteSelection()
})
const sel = editorSession.getSelection()
t.equal(l1.items.length, 1, 'The list should have 1 item')
const li1 = l1.getItemAt(0)
t.equal(li1.getText(), LI1_TEXT.slice(0, 3) + LI2_TEXT.slice(3), 'The items should be merged')
t.ok(sel.isCollapsed(), 'Selection should be collapsed')
t.deepEqual(sel.start.path, li1.getPath(), '... on the list item')
t.equal(sel.start.offset, 3, '... at correct position')
t.end()
})
test('Editing: DEL28: Deleting a ContainerSelection within a single TextNode', (t) => {
const { editorSession, doc } = setupEditor(t, _p1)
const p1 = doc.get('p1')
editorSession.setSelection({
type: 'container',
startPath: p1.getPath(),
startOffset: 3,
endPath: p1.getPath(),
endOffset: 5,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteSelection()
})
t.equal(p1.getText(), P1_TEXT.slice(0, 3) + P1_TEXT.slice(5), 'The text should have been deleted')
_checkSelection(t, editorSession.getSelection(), {
type: 'property',
path: p1.getPath(),
startOffset: 3
})
t.end()
})
test('Editing: DEL29: Deleting a character inside an IsolatedNode', (t) => {
const { doc, editorSession } = setupEditor(t, _p1, _in1, _p2)
editorSession.setSelection({
type: 'property',
path: ['in1', 'title'],
startOffset: 3
})
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
const in1 = doc.get('in1')
t.equal(in1.title, IN1_TITLE.slice(0, 3) + IN1_TITLE.slice(4), 'Text should be inserted into field')
t.end()
})
test('Editing: DEL30: Merging two ListItems using DELETE', (t) => {
const { editorSession, doc } = setupEditor(t, _l1, _l11, _l12)
editorSession.setSelection({
type: 'property',
path: ['l1-1', 'content'],
startOffset: LI1_TEXT.length,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
const sel = editorSession.getSelection()
const l = doc.get('l1')
t.equal(l.items.length, 1, 'Only one list item should be left')
const li = l.getItemAt(0)
t.equal(li.getText(), LI1_TEXT + LI2_TEXT, 'The list item should have the merged text')
t.ok(sel.isCollapsed(), 'The selection should be collapsed')
t.deepEqual(sel.start.path, li.getPath(), '... on the list item')
t.equal(sel.start.offset, LI1_TEXT.length, '... at the end of the original content')
t.end()
})
test('Editing: DEL31: Merging a List into previous List using DELETE', (t) => {
const { editorSession, doc } = setupEditor(t, _l1, _l11, _l12, _l2, _l21, _l22)
editorSession.setSelection({
type: 'property',
path: ['l1-2', 'content'],
startOffset: LI2_TEXT.length,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
const sel = editorSession.getSelection()
const l1 = doc.get('l1')
t.equal(l1.items.length, 4, 'First list should have 4 items')
t.ok(sel.isCollapsed(), 'The selection should be collapsed')
t.deepEqual(sel.start.path, ['l1-2', 'content'], '... on the same item')
t.equal(sel.start.offset, LI2_TEXT.length, '... at the same position')
t.end()
})
test('Editing: DEL31-2: Merging a List into previous List using DELETE on an empty paragraph in between', (t) => {
const { editorSession, doc } = setupEditor(t, _l1, _l11, _l12, _empty, _l2, _l21, _l22)
editorSession.setSelection({
type: 'property',
path: ['empty', 'content'],
startOffset: 0,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('left')
})
const sel = editorSession.getSelection()
const body = doc.get('body')
const l1 = doc.get('l1')
t.equal(body.length, 1, 'There should only be one node on the top level')
t.equal(l1.items.length, 4, 'First list should have 4 items')
t.ok(sel.isCollapsed(), 'The selection should be collapsed')
t.deepEqual(sel.start.path, ['l1-2', 'content'], '... on the second list item')
t.equal(sel.start.offset, LI2_TEXT.length, '... at the last position')
t.end()
})
test('Editing: DEL32: Merging an empty List into previous TextNode using DELETE', (t) => {
const { editorSession, doc } = setupEditor(t, _p1, _l1)
const p1 = doc.get('p1')
editorSession.setSelection({
type: 'property',
path: p1.getPath(),
startOffset: p1.getLength(),
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
t.isNil(doc.get('l2'), 'Second list should have been removed.')
t.end()
})
test('Editing: DEL33: Deleting using BACKSPACE at the start of a text property editor', (t) => {
const { editorSession, doc } = setupEditor(t)
const p = doc.create({
id: 'p',
type: 'paragraph',
content: 'foo'
})
editorSession.setSelection({
type: 'property',
path: p.getPath(),
startOffset: 0,
endOffset: 0
})
t.doesNotThrow(() => {
editorSession.transaction((tx) => {
tx.deleteCharacter('left')
})
}, 'Should not throw an exception')
t.equal(p.getText(), 'foo', '.. and the text should be untouched')
t.end()
})
test('Editing: DEL34: Deleting using DEL at the end of a text property editor', (t) => {
const { editorSession, doc } = setupEditor(t)
const p = doc.create({
id: 'p',
type: 'paragraph',
content: 'foo'
})
editorSession.setSelection({
type: 'property',
path: p.getPath(),
startOffset: 3,
endOffset: 3
})
t.doesNotThrow(() => {
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
}, 'Should not throw an exception')
t.equal(p.getText(), 'foo', '.. and the text should be untouched')
t.end()
})
test('Editing: DEL35: Deleting a NodeSelection', (t) => {
const { editorSession } = setupEditor(t, _in1, doc => {
doc.create({
type: 'strong',
id: 'in1-body-p1-s1',
start: {
path: ['in1-body-p1', 'content'],
offset: 2
},
end: {
offset: 4
}
})
})
editorSession.setSelection({
type: 'node',
nodeId: 'in1',
containerPath: ['body', 'nodes']
})
const change = editorSession.transaction((tx) => {
tx.deleteSelection()
})
const deleteOps = change.ops.filter(op => op.isDelete())
const deletedIds = deleteOps.map(op => op.path[0])
const expected = ['in1', 'in1-body-p1-s1', 'in1-body-p1']
t.deepEqual(deletedIds, expected, 'The nodes should have been deleted hierarchically and in correct order.')
t.end()
})
test('Editing: DEL36: Deleting unicode using BACKSPACE', (t) => {
const { editorSession, doc } = setupEditor(t)
const TEXT = 'Text with unicode:'
const POO = '💩'
const TEXT_AND_POO = TEXT + POO
const p = doc.create({
id: 'p',
type: 'paragraph',
content: TEXT + POO
})
editorSession.setSelection({
type: 'property',
path: p.getPath(),
startOffset: TEXT_AND_POO.length,
endOffset: TEXT_AND_POO.length
})
t.doesNotThrow(() => {
editorSession.transaction((tx) => {
tx.deleteCharacter('left')
})
}, 'Should not throw an exception')
t.equal(p.getText(), 'Text with unicode:', '.. unicode character should have been deleted')
t.end()
})
test('Editing: DEL37: Deleting unicode using DEL', (t) => {
const { editorSession, doc } = setupEditor(t)
const TEXT = 'Text with unicode:'
const POO = '💩'
const p = doc.create({
id: 'p',
type: 'paragraph',
content: TEXT + POO
})
editorSession.setSelection({
type: 'property',
path: p.getPath(),
startOffset: TEXT.length,
endOffset: TEXT.length
})
t.doesNotThrow(() => {
editorSession.transaction((tx) => {
tx.deleteCharacter('right')
})
}, 'Should not throw an exception')
t.equal(p.getText(), TEXT, '.. unicode character should have been deleted')
t.end()
})
test('Editing: BR1: Breaking without selection', (t) => {
const { editorSession } = setupEditor(t, _p1)
editorSession.setSelection(null)
t.doesNotThrow(() => {
editorSession.transaction((tx) => {
tx.break()
})
})
t.end()
})
test('Editing: BR2: Breaking a TextNode', (t) => {
const { editorSession, doc } = setupEditor(t, _p1)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 3,
containerPath: ['body', 'nodes']
})
editorSession.transaction((tx) => {
tx.break()
}, { action: 'break' })
const body = doc.get('body')
t.equal(body.length, 2, 'There should be 2 nodes')
const first = body.getNodeAt(0)
const second = body.getNodeAt(1)
t.equal(second.type, 'paragraph', 'Second should be a paragraph')
t.equal(first.getText(), P1_TEXT.slice(0, 3), 'First should be truncated')
t.equal(second.getText(), P1_TEXT.slice(3), '.. and second should contain the tail')
t.end()
})
test('Editing: BR3: Breaking annotated text with cursor before the annotation', (t) => {