UNPKG

codemirror-ot

Version:

Operational Transformation adapter for CodeMirror 6.

273 lines (242 loc) 8.4 kB
import * as assert from 'assert'; import json1 from 'ot-json1'; import textUnicode from 'ot-text-unicode'; import { EditorState, ChangeSet } from '@codemirror/state'; import { EditorView, ViewPlugin } from '@codemirror/view'; import { JSDOM } from 'jsdom'; import ShareDB from 'sharedb'; import { json1Sync, canOpAffectPath } from '../src/index'; ShareDB.types.register(json1.type); // Gets a value at a path in a ShareDB Doc. const getAtPath = (shareDBDoc, path) => path.reduce((accumulator, key) => accumulator[key], shareDBDoc.data); // Set up stuff in Node so that EditorView works. // Inspired by https://github.com/yjs/y-codemirror.next/blob/main/test/test.node.cjs const { window } = new JSDOM(''); ['window', 'innerWidth', 'innerHeight', 'document', 'MutationObserver'].forEach( (name) => { global[name] = window[name]; }, ); // Make these available where CodeMirror looks for them. // See https://github.com/codemirror/view/blob/main/src/editorview.ts#L119 // this.win is used for requestAnimationFrame global.window.requestAnimationFrame = (f) => setTimeout(f, 0); // this.win is _not_ used for cancelAnimationFrame global.cancelAnimationFrame = () => {}; // Creates a new CodeMirror EditorView with the json1Sync extension set up. const createEditor = ({ shareDBDoc, path, additionalExtensions = [] }) => { const view = new EditorView({ state: EditorState.create({ doc: getAtPath(shareDBDoc, path), extensions: [ json1Sync({ shareDBDoc, path, json1, textUnicode }), ...additionalExtensions, ], }), }); return view; }; export const testIntegration = () => { describe('Mocked ShareDB', () => { const setupTestEnvironment = (text) => { const environment = {}; createEditor({ shareDBDoc: { data: { content: { files: { 2432: { text } } } }, submitOp: (op) => { environment.submittedOp = op; }, on: (eventName, callback) => { if (eventName === 'op') { environment.receiveOp = callback; } }, }, path: ['content', 'files', '2432', 'text'], additionalExtensions: [ ViewPlugin.fromClass( class { update(update) { environment.changes = update.changes; } }, ), ], }); return environment; }; it('ShareDB --> CodeMirror', () => { const text = 'Hello World'; const environment = setupTestEnvironment(text); // Simulate ShareDB receiving a remote op. environment.receiveOp([ 'content', 'files', '2432', 'text', { es: [5, '-', { d: ' ' }] }, ]); // Verify the remote op was translated to a CodeMirror change and dispatched to the editor view. assert.deepEqual( environment.changes.toJSON(), ChangeSet.of([{ from: 5, to: 6, insert: '-' }], text.length).toJSON(), ); // Verify that the extension did _not_ submit the received ShareDB op back into ShareDB. assert.equal(environment.submittedOp, undefined); }); it('ShareDB --> CodeMirror, multi-part ops', () => { const text = 'Hello World'; const environment = setupTestEnvironment(text); // Simulate ShareDB receiving a remote op. environment.receiveOp([ ['content', 'files', '2432', 'text', { es: [5, '-', { d: ' ' }] }], ['isInteracting', { r: true }], ]); // Verify the remote op was translated to a CodeMirror change and dispatched to the editor view. assert.deepEqual( environment.changes.toJSON(), ChangeSet.of([{ from: 5, to: 6, insert: '-' }], text.length).toJSON(), ); // Verify that the extension did _not_ submit the received ShareDB op back into ShareDB. assert.equal(environment.submittedOp, undefined); }); it('ShareDB --> CodeMirror, null ops', () => { const text = 'Hello World'; const environment = setupTestEnvironment(text); // Simulate ShareDB receiving a remote op. environment.receiveOp(null); assert.equal(environment.changes, undefined); assert.equal(environment.submittedOp, undefined); }); it('Clear entire document ShareDB --> CodeMirror', () => { const text = 'Hello World'; const environment = setupTestEnvironment(text); // Simulate ShareDB receiving a remote op that clears the document environment.receiveOp([ 'content', 'files', '2432', 'text', { es: [{ d: 'Hello World' }] }, ]); // Verify the document was cleared assert.deepEqual( environment.changes.toJSON(), ChangeSet.of([{ from: 0, to: 11 }], text.length).toJSON(), ); assert.equal(environment.submittedOp, undefined); }); }); describe('Real ShareDB', () => { // Create initial document then fire callback it('CodeMirror --> ShareDB', (done) => { const backend = new ShareDB(); const connection = backend.connect(); const shareDBDoc = connection.get('testCollection', 'testDocId'); shareDBDoc.create( { content: { files: { 2432: { text: 'Hello World' } } } }, json1.type.uri, () => { shareDBDoc.on('op', (op) => { assert.deepEqual(op, [ 'content', 'files', '2432', 'text', { es: [5, '-', { d: ' ' }] }, ]); done(); }); shareDBDoc.subscribe(() => { const editor = createEditor({ shareDBDoc, path: ['content', 'files', '2432', 'text'], }); editor.dispatch({ changes: [{ from: 5, to: 6, insert: '-' }] }); }); }, ); }); it('ShareDB --> CodeMirror', (done) => { const backend = new ShareDB(); const connection = backend.connect(); const shareDBDoc = connection.get('testCollection', 'testDocId'); const text = 'Hello World'; shareDBDoc.create( { content: { files: { 2432: { text }, otherFile: { text: 'HelloWorld' } }, }, }, json1.type.uri, () => { shareDBDoc.subscribe(() => { createEditor({ shareDBDoc, path: ['content', 'files', '2432', 'text'], additionalExtensions: [ ViewPlugin.fromClass( class { update(update) { // verify that the remote op was translated to a CodeMirror change // and dispatched to the editor view. assert.deepEqual( update.changes.toJSON(), ChangeSet.of( [{ from: 5, to: 6, insert: '-' }], text.length, ).toJSON(), ); done(); } }, ), ], }); // Simulate ShareDB receiving a remote op. //console.log(shareDBDoc.data) shareDBDoc.submitOp([ 'content', 'files', '2432', 'text', { es: [5, '-', { d: '-' }] }, ]); // TODO add test for ops coming in for an irrelevant path // (test the canOpAffectPath invocation). }); }, ); }); }); describe('canOpAffectPath', () => { it('true', () => { const op = [ 'content', 'files', '2432', 'text', { es: [5, '-', { d: '-' }] }, ]; const path = ['content', 'files', '2432', 'text']; assert.deepEqual(canOpAffectPath(op, path), true); }); it('false', () => { const op = [ 'content', 'files', 'other-file', 'text', { es: [5, '-', { d: '-' }] }, ]; const path = ['content', 'files', '2432', 'text']; assert.deepEqual(canOpAffectPath(op, path), false); }); it('null op case', () => { const op = null; const path = ['content', 'files', '2432', 'text']; assert.deepEqual(canOpAffectPath(op, path), false); }); }); };