marshall-y-slate
Version:
Yjs bindings for Slate.
392 lines (382 loc) • 12.2 kB
text/typescript
import { toSlateDoc } from '../src';
import { CustomNode } from '../src/model';
import { TestEditor, TransformFunc } from './test-editor';
import { createNode, createTestEditor, createValue, wait } from './utils';
const tests = [
[
'Insert text into a paragraph node',
[createNode('paragraph', '')],
[
TestEditor.makeInsertText('Hello ', { path: [0, 0], offset: 0 }),
TestEditor.makeInsertText('collaborator', { path: [0, 0], offset: 6 }),
TestEditor.makeInsertText('!', { path: [0, 0], offset: 18 })
],
[createNode('paragraph', 'Hello collaborator!')]
],
[
'Delete characters from a paragraph node',
[createNode('paragraph', 'Hello collaborator!')],
[
TestEditor.makeRemoveCharacters(7, { path: [0, 0], offset: 11 }),
TestEditor.makeRemoveCharacters(6, { path: [0, 0], offset: 5 })
],
[createNode('paragraph', 'Hello!')]
],
[
'Insert new nodes (both paragraph and text)',
[createNode()],
[
TestEditor.makeInsertNodes({ type: 'paragraph', children: [] }, [1]),
TestEditor.makeInsertNodes({ text: 'Hello collaborator!' }, [1, 0])
],
[createNode(), createNode('paragraph', 'Hello collaborator!')]
],
[
'Insert new nodes (two paragraphs)',
[
createNode('paragraph', 'alfa'),
createNode('paragraph', 'bravo'),
createNode('paragraph', 'charlie'),
createNode('paragraph', 'delta')
],
[
TestEditor.makeInsertNodes(createNode('paragraph', 'echo'), [1]),
TestEditor.makeInsertNodes(createNode('paragraph', 'foxtrot'), [4])
],
[
createNode('paragraph', 'alfa'),
createNode('paragraph', 'echo'),
createNode('paragraph', 'bravo'),
createNode('paragraph', 'charlie'),
createNode('paragraph', 'foxtrot'),
createNode('paragraph', 'delta')
]
],
[
'Insert new nodes (three paragraphs)',
[
createNode('paragraph', 'one'),
createNode('paragraph', 'two'),
createNode('paragraph', 'three'),
createNode('paragraph', 'four')
],
[
TestEditor.makeInsertNodes(createNode('paragraph', 'five'), [1]),
TestEditor.makeInsertNodes(createNode('paragraph', 'six'), [3]),
TestEditor.makeInsertNodes(createNode('paragraph', 'seven'), [5])
],
[
createNode('paragraph', 'one'),
createNode('paragraph', 'five'),
createNode('paragraph', 'two'),
createNode('paragraph', 'six'),
createNode('paragraph', 'three'),
createNode('paragraph', 'seven'),
createNode('paragraph', 'four')
]
],
[
'Merge two paragraph nodes',
[
createNode('paragraph', 'Hello '),
createNode('paragraph', 'collaborator!')
],
[TestEditor.makeMergeNodes([1])],
[createNode('paragraph', 'Hello collaborator!')]
],
[
'Move a paragraph node to an existing position',
[
createNode('paragraph', 'first'),
createNode('paragraph', 'second'),
createNode('paragraph', 'third')
],
[TestEditor.makeMoveNodes([1], [0])],
[
createNode('paragraph', 'second'),
createNode('paragraph', 'first'),
createNode('paragraph', 'third')
]
],
[
'Move a paragraph node to the end',
[
createNode('paragraph', 'first'),
createNode('paragraph', 'second'),
createNode('paragraph', 'third')
],
[TestEditor.makeMoveNodes([0], [3])],
[
createNode('paragraph', 'second'),
createNode('paragraph', 'third'),
createNode('paragraph', 'first')
]
],
[
'Move a paragraph node far past the end',
[
createNode('paragraph', 'first'),
createNode('paragraph', 'second'),
createNode('paragraph', 'third')
],
[TestEditor.makeMoveNodes([0], [1000])],
[
createNode('paragraph', 'second'),
createNode('paragraph', 'third'),
createNode('paragraph', 'first')
]
],
[
'Move a text node',
[
createNode('paragraph', 'first'),
createNode('paragraph', 'second'),
createNode('paragraph', 'third')
],
[TestEditor.makeMoveNodes([1, 0], [2, 0])],
[
createNode('paragraph', 'first'),
createNode('paragraph', ''),
createNode('paragraph', 'secondthird')
]
],
[
'Remove a paragraph node',
[
createNode('paragraph', 'first'),
createNode('paragraph', 'second'),
createNode('paragraph', 'third')
],
[TestEditor.makeRemoveNodes([0])],
[createNode('paragraph', 'second'), createNode('paragraph', 'third')]
],
[
'Remove two non-consecutive paragraph nodes',
[
createNode('paragraph', 'first'),
createNode('paragraph', 'second'),
createNode('paragraph', 'third'),
createNode('paragraph', 'fourth')
],
[TestEditor.makeRemoveNodes([0]), TestEditor.makeRemoveNodes([1])],
[createNode('paragraph', 'second'), createNode('paragraph', 'fourth')]
],
[
'Remove two consecutive paragraph nodes',
[
createNode('paragraph', 'first'),
createNode('paragraph', 'second'),
createNode('paragraph', 'third'),
createNode('paragraph', 'fourth')
],
[TestEditor.makeRemoveNodes([1]), TestEditor.makeRemoveNodes([1])],
[createNode('paragraph', 'first'), createNode('paragraph', 'fourth')]
],
[
'Remove a text node',
[
createNode('paragraph', 'first'),
createNode('paragraph', 'second'),
createNode('paragraph', 'third')
],
[TestEditor.makeRemoveNodes([1, 0])],
[
createNode('paragraph', 'first'),
createNode('paragraph', ''),
createNode('paragraph', 'third')
]
],
[
'Set properties of a paragraph node',
[
createNode('paragraph', 'first', { test: '1234' }),
createNode('paragraph', 'second')
],
[TestEditor.makeSetNodes([0], { test: '4567' })],
[
createNode('paragraph', 'first', { test: '4567' }),
createNode('paragraph', 'second')
]
],
[
'Set properties of a text node',
[
createNode('paragraph', 'first', { test: '1234' }),
createNode('paragraph', 'second')
],
[TestEditor.makeSetNodes([1, 0], { data: '4567' })],
[
createNode('paragraph', 'first', { test: '1234' }),
{
type: 'paragraph',
children: [
{
data: '4567',
text: 'second'
}
]
}
]
],
[
'Split an existing paragraph',
[createNode('paragraph', 'Hello collaborator!')],
[TestEditor.makeSplitNodes({ path: [0, 0], offset: 6 })],
[
createNode('paragraph', 'Hello '),
createNode('paragraph', 'collaborator!')
]
],
[
'Insert and remove text in the same paragraph',
[createNode('paragraph', 'abc def')],
[
TestEditor.makeInsertText('ghi ', { path: [0, 0], offset: 4 }),
TestEditor.makeRemoveCharacters(2, { path: [0, 0], offset: 1 }),
TestEditor.makeInsertText('jkl ', { path: [0, 0], offset: 6 }),
TestEditor.makeRemoveCharacters(1, { path: [0, 0], offset: 11 }),
TestEditor.makeInsertText(' mno', { path: [0, 0], offset: 12 })
],
[createNode('paragraph', 'a ghi jkl df mno')]
],
[
'Remove first paragraph, insert text into second paragraph',
[createNode('paragraph', 'abcd'), createNode('paragraph', 'efgh')],
[
TestEditor.makeRemoveNodes([0]),
TestEditor.makeInsertText(' ijkl ', { path: [0, 0], offset: 2 })
],
[createNode('paragraph', 'ef ijkl gh')]
],
[
'More complex case: insert text, both insert and remove nodes',
[
createNode('paragraph', 'abcd'),
createNode('paragraph', 'efgh'),
createNode('paragraph', 'ijkl')
],
[
TestEditor.makeInsertText(' mnop ', { path: [0, 0], offset: 2 }),
TestEditor.makeRemoveNodes([1]),
TestEditor.makeInsertText(' qrst ', { path: [1, 0], offset: 2 }),
TestEditor.makeInsertNodes(createNode('paragraph', 'uvxw'), [1])
],
[
createNode('paragraph', 'ab mnop cd'),
createNode('paragraph', 'uvxw'),
createNode('paragraph', 'ij qrst kl')
]
],
[
'Insert text, then insert node that affects the path to the affected text',
[createNode('paragraph', 'abcd')],
[
TestEditor.makeInsertText(' efgh ', { path: [0, 0], offset: 2 }),
TestEditor.makeInsertNodes(createNode('paragraph', 'ijkl'), [0])
],
[createNode('paragraph', 'ijkl'), createNode('paragraph', 'ab efgh cd')]
],
[
'Set properties, then insert node that affects path to the affected node',
[createNode('paragraph', 'abcd')],
[
TestEditor.makeSetNodes([0], { test: '1234' }),
TestEditor.makeInsertNodes(createNode('paragraph', 'ijkl'), [0])
],
[
createNode('paragraph', 'ijkl'),
createNode('paragraph', 'abcd', { test: '1234' })
]
],
[
'Insert node, then insert second node that affects path to the first node',
[
createValue([
createNode('paragraph', 'abc'),
createNode('paragraph', 'def')
])
],
[
TestEditor.makeInsertNodes(createNode('paragraph', 'jkl'), [0, 1]),
TestEditor.makeInsertNodes(createNode('paragraph', 'ghi'), [0])
],
[
createNode('paragraph', 'ghi'),
createValue([
createNode('paragraph', 'abc'),
createNode('paragraph', 'jkl'),
createNode('paragraph', 'def')
])
]
],
[
'remove_text op spans location of previous remove_text op',
[createNode('paragraph', 'abc defg ijklm')],
[TestEditor.makeRemoveCharacters(5, { path: [0, 0], offset: 4 })],
[createNode('paragraph', 'abc ijklm')],
[TestEditor.makeRemoveCharacters(6, { path: [0, 0], offset: 1 })],
[createNode('paragraph', 'alm')]
],
[
'remove_text op spans locations of two previous remove_text ops',
[createNode('paragraph', 'abcdefghijklmnopqrst')],
[TestEditor.makeRemoveCharacters(3, { path: [0, 0], offset: 2 })],
[createNode('paragraph', 'abfghijklmnopqrst')],
[TestEditor.makeRemoveCharacters(8, { path: [0, 0], offset: 5 })],
[createNode('paragraph', 'abfghqrst')],
[TestEditor.makeRemoveCharacters(7, { path: [0, 0], offset: 1 })],
[createNode('paragraph', 'at')]
],
[
'set_selection op is a null op',
[createNode('paragraph', 'abcdefghijklmnopqrst')],
[
TestEditor.makeSetSelection(
{ path: [0, 0], offset: 2 },
{ path: [0, 0], offset: 4 }
)
],
[createNode('paragraph', 'abcdefghijklmnopqrst')]
]
];
describe('slate operations propagate between editors', () => {
tests.forEach(([testName, input, ...cases]) => {
it(`${testName}`, async () => {
// Create two editors.
const src = createTestEditor();
const dst = createTestEditor();
// Set initial state for src editor, propagate changes to dst editor.
TestEditor.applyTransform(
src,
TestEditor.makeInsertNodes(input as CustomNode[], [0])
);
await wait();
let updates = TestEditor.getCapturedYjsUpdates(src);
TestEditor.applyYjsUpdatesToYjs(dst, updates);
await wait();
// Verify initial states.
expect(src.children).toEqual(input);
expect(toSlateDoc(src.sharedType)).toEqual(input);
expect(toSlateDoc(dst.sharedType)).toEqual(input);
expect(dst.children).toEqual(input);
// Allow for multiple rounds of applying transforms and verifying state.
const toTest = cases.slice();
while (toTest.length > 0) {
const [transforms, output] = toTest.splice(0, 2);
// Apply transforms to src editor, propagate changes to dst editor.
TestEditor.applyTransforms(src, transforms as TransformFunc[]);
// eslint-disable-next-line no-await-in-loop
await wait();
updates = TestEditor.getCapturedYjsUpdates(src);
TestEditor.applyYjsUpdatesToYjs(dst, updates);
// eslint-disable-next-line no-await-in-loop
await wait();
// Verify final states.
expect(src.children).toEqual(output);
expect(toSlateDoc(src.sharedType)).toEqual(output);
expect(toSlateDoc(dst.sharedType)).toEqual(output);
expect(dst.children).toEqual(output);
}
});
});
});