diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.render-inline.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.render-inline.spec.ts new file mode 100644 index 0000000000..4d03a06644 --- /dev/null +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.render-inline.spec.ts @@ -0,0 +1,280 @@ +import {render} from './render'; +import { + Kit, + setupNumbersKit, + setupNumbersWithMultipleChunksAndDeletesKit, + setupNumbersWithRgaSplitKit, + setupNumbersWithTombstonesKit, + setupNumbersWithTwoChunksKit, +} from './setup'; + +const runTests = (_setup: () => Kit) => { + const setup = () => { + const kit = _setup(); + const view = () => { + kit.peritext.editor.delCursors(); + kit.peritext.refresh(); + return render(kit.peritext.blocks.root); + }; + return {...kit, view}; + }; + + test('renders plain text', () => { + const {view} = setup(); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "0123456789" { } +" +`); + }); + + test('can annotate beginning of text', () => { + const {editor, view} = setup(); + editor.cursor.setAt(0, 3); + editor.saved.insOverwrite('BOLD'); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "" { } + "012" { BOLD = [ 1, 3 ] } + "3456789" { } +" +`); + }); + + test('can annotate middle of text', () => { + const {editor, view} = setup(); + editor.cursor.setAt(3, 3); + editor.saved.insOverwrite('BOLD'); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "012" { } + "345" { BOLD = [ 1, 3 ] } + "6789" { } +" +`); + }); + + test('can annotate end of text', () => { + const {editor, view} = setup(); + editor.cursor.setAt(7, 3); + editor.saved.insOverwrite('ITALIC'); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "0123456" { } + "789" { ITALIC = [ 1, 3 ] } + "" { } +" +`); + }); + + test('can annotate two regions', () => { + const {editor, view} = setup(); + editor.cursor.setAt(1, 2); + editor.saved.insOverwrite('BOLD'); + editor.cursor.setAt(5, 3); + editor.saved.insOverwrite('ITALIC'); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "0" { } + "12" { BOLD = [ 1, 3 ] } + "34" { } + "567" { ITALIC = [ 1, 3 ] } + "89" { } +" +`); + }); + + test('can annotate two adjacent regions', () => { + const {editor, view} = setup(); + editor.cursor.setAt(0, 2); + editor.saved.insOverwrite('BOLD'); + editor.cursor.setAt(2, 3); + editor.saved.insOverwrite('ITALIC'); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "" { } + "01" { BOLD = [ 1, 3 ] } + "" { } + "234" { ITALIC = [ 1, 3 ] } + "56789" { } +" +`); + }); + + test('can annotate two adjacent regions at the end of text', () => { + const {editor, view} = setup(); + editor.cursor.setAt(5, 2); + editor.saved.insOverwrite('BOLD'); + editor.cursor.setAt(7, 3); + editor.saved.insOverwrite('ITALIC'); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "01234" { } + "56" { BOLD = [ 1, 3 ] } + "" { } + "789" { ITALIC = [ 1, 3 ] } + "" { } +" +`); + }); + + test('can annotate overlapping regions at the beginning of text', () => { + const {editor, view} = setup(); + editor.cursor.setAt(0, 2); + editor.saved.insOverwrite('BOLD'); + editor.cursor.setAt(1, 2); + editor.saved.insOverwrite('ITALIC'); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "" { } + "0" { BOLD = [ 1, 1 ] } + "1" { BOLD = [ 1, 2 ], ITALIC = [ 1, 1 ] } + "2" { ITALIC = [ 1, 2 ] } + "3456789" { } +" +`); + }); + + test('can annotate overlapping regions in the middle of text', () => { + const {editor, view} = setup(); + editor.cursor.setAt(4, 2); + editor.saved.insOverwrite('BOLD'); + editor.cursor.setAt(5, 2); + editor.saved.insOverwrite('ITALIC'); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "0123" { } + "4" { BOLD = [ 1, 1 ] } + "5" { BOLD = [ 1, 2 ], ITALIC = [ 1, 1 ] } + "6" { ITALIC = [ 1, 2 ] } + "789" { } +" +`); + }); + + test('can annotate a contained region at the beginning of text', () => { + const {editor, view} = setup(); + editor.cursor.setAt(0, 5); + editor.saved.insOverwrite('BOLD'); + editor.cursor.setAt(1, 2); + editor.saved.insOverwrite('ITALIC'); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "" { } + "0" { BOLD = [ 1, 1 ] } + "12" { BOLD = [ 1, 0 ], ITALIC = [ 1, 3 ] } + "34" { BOLD = [ 1, 2 ] } + "56789" { } +" +`); + }); + + test('can annotate twice contained region in the middle of text', () => { + const {editor, view} = setup(); + editor.cursor.setAt(4, 5); + editor.saved.insOverwrite('BOLD'); + editor.cursor.setAt(5, 3); + editor.saved.insOverwrite('ITALIC'); + editor.cursor.setAt(6, 1); + editor.saved.insOverwrite('UNDERLINE'); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "0123" { } + "4" { BOLD = [ 1, 1 ] } + "5" { BOLD = [ 1, 0 ], ITALIC = [ 1, 1 ] } + "6" { BOLD = [ 1, 0 ], ITALIC = [ 1, 0 ], UNDERLINE = [ 1, 3 ] } + "7" { BOLD = [ 1, 0 ], ITALIC = [ 1, 2 ] } + "8" { BOLD = [ 1, 2 ] } + "9" { } +" +`); + }); + + test('can annotate twice contained region at the end of text', () => { + const {editor, view} = setup(); + editor.cursor.setAt(5, 5); + editor.saved.insOverwrite('BOLD'); + editor.cursor.setAt(6, 3); + editor.saved.insOverwrite('ITALIC'); + editor.cursor.setAt(7, 1); + editor.saved.insOverwrite('UNDERLINE'); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "01234" { } + "5" { BOLD = [ 1, 1 ] } + "6" { BOLD = [ 1, 0 ], ITALIC = [ 1, 1 ] } + "7" { BOLD = [ 1, 0 ], ITALIC = [ 1, 0 ], UNDERLINE = [ 1, 3 ] } + "8" { BOLD = [ 1, 0 ], ITALIC = [ 1, 2 ] } + "9" { BOLD = [ 1, 2 ] } + "" { } +" +`); + }); + + test('can annotate three intermingled regions', () => { + const {editor, view} = setup(); + editor.cursor.setAt(2, 6); + editor.saved.insOverwrite('BOLD'); + editor.cursor.setAt(1, 5); + editor.saved.insOverwrite('ITALIC'); + editor.cursor.setAt(4, 5); + editor.saved.insOverwrite('UNDERLINE'); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "0" { } + "1" { ITALIC = [ 1, 1 ] } + "23" { BOLD = [ 1, 1 ], ITALIC = [ 1, 0 ] } + "45" { BOLD = [ 1, 0 ], ITALIC = [ 1, 2 ], UNDERLINE = [ 1, 1 ] } + "67" { BOLD = [ 1, 2 ], UNDERLINE = [ 1, 0 ] } + "8" { UNDERLINE = [ 1, 2 ] } + "9" { } +" +`); + }); + + test('can insert zero length slice', () => { + const {editor, view} = setup(); + editor.cursor.setAt(2, 0); + editor.saved.insOverwrite('CURSOR'); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "01" { } + "23456789" { CURSOR = [ 1, 4 ] } +" +`); + }); +}; + +describe('numbers "0123456789", no edits', () => { + runTests(setupNumbersKit); +}); + +describe('numbers "0123456789", with default schema and tombstones', () => { + runTests(setupNumbersWithTombstonesKit); +}); + +describe('numbers "0123456789", two RGA chunks', () => { + runTests(setupNumbersWithTwoChunksKit); +}); + +describe('numbers "0123456789", with RGA split', () => { + runTests(setupNumbersWithRgaSplitKit); +}); + +describe('numbers "0123456789", with multiple deletes', () => { + runTests(setupNumbersWithMultipleChunksAndDeletesKit); +}); diff --git a/src/json-crdt-extensions/peritext/__tests__/setup.ts b/src/json-crdt-extensions/peritext/__tests__/setup.ts index 7a2e2832f1..38180b8fd0 100644 --- a/src/json-crdt-extensions/peritext/__tests__/setup.ts +++ b/src/json-crdt-extensions/peritext/__tests__/setup.ts @@ -58,7 +58,7 @@ export const setupNumbersKit = (): Kit => { return setupKit('', (model) => { const str = model.s.text.toExt().text(); str.ins(0, '0123456789'); - if (str.view() !== '0123456789') throw new Error('Invalid text'); + if (str.view() !== '0123456789') throw new Error('Invalid text: ' + str.view()); }); }; @@ -98,8 +98,104 @@ export const setupNumbersWithTombstonesKit = (sid?: number): Kit => { str.ins(2, 'x234'); str.del(2, 1); str.del(10, 3); - if (str.view() !== '0123456789') throw new Error('Invalid text'); + if (str.view() !== '0123456789') throw new Error('Invalid text: ' + str.view()); }, sid, ); }; + +/** + * Creates a Peritext instance with text "0123456789", two RGA chunks. + */ +export const setupNumbersWithTwoChunksKit = (): Kit => { + return setupKit('', (model) => { + const str = model.s.text.toExt().text(); + str.ins(0, '56789'); + str.ins(0, '01234'); + if (str.view() !== '0123456789') throw new Error('Invalid text: ' + str.view()); + }); +}; + +/** + * Creates a Peritext instance with text "0123456789", with RGA chunks split. + */ +export const setupNumbersWithRgaSplitKit = (): Kit => { + return setupKit('', (model) => { + const str = model.s.text.toExt().text(); + str.ins(0, '012389'); + str.ins(4, '4567'); + if (str.view() !== '0123456789') throw new Error('Invalid text: ' + str.view()); + }); +}; + +/** + * Creates a Peritext instance with text "0123456789", with multiple chunks and deletes. + */ +export const setupNumbersWithMultipleChunksAndDeletesKit = (): Kit => { + return setupKit('', (model) => { + const str = model.s.text.toExt().text(); + str.ins(0, '0'); + str.ins(1, '1'); + str.ins(2, '2xyz3'); + str.del(3, 3); + str.ins(4, '4589'); + str.ins(6, '67'); + str.ins(8, 'cool worlds'); + str.del(8, 11); + if (str.view() !== '0123456789') throw new Error('Invalid text: ' + str.view()); + }); +}; + +/** + * Creates a Peritext instance with text "abcdefghijklmnopqrstuvwxyz", no edits. + */ +export const setupAlphabetKit = (): Kit => { + return setupKit('', (model) => { + const str = model.s.text.toExt().text(); + str.ins(0, 'abcdefghijklmnopqrstuvwxyz'); + if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text'); + }); +}; + +/** + * Creates a Peritext instance with text "abcdefghijklmnopqrstuvwxyz", two text chunks. + */ +export const setupAlphabetWithTwoChunksKit = (): Kit => { + return setupKit('', (model) => { + const str = model.s.text.toExt().text(); + str.ins(0, 'lmnopqrstuvwxyz'); + str.ins(0, 'abcdefghijk'); + if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text'); + }); +}; + +/** + * Creates a Peritext instance with text "abcdefghijklmnopqrstuvwxyz", with RGA chunks split. + */ +export const setupAlphabetChunkSplitKit = (): Kit => { + return setupKit('', (model) => { + const str = model.s.text.toExt().text(); + str.ins(0, 'lmnwxyz'); + str.ins(3, 'opqrstuv'); + str.ins(0, 'abcdefghijk'); + if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text'); + }); +}; + +/** + * Creates a Peritext instance with text "abcdefghijklmnopqrstuvwxyz", with RGA deletes. + */ +export const setupAlphabetWithDeletesKit = (): Kit => { + return setupKit('', (model) => { + const str = model.s.text.toExt().text(); + str.ins(0, 'lmXXXnwYxyz'); + str.del(2, 3); + str.ins(3, 'opqrstuv'); + str.del(12, 1); + str.ins(0, 'ab1c3defghijk4444'); + str.del(2, 1); + str.del(3, 1); + str.del(11, 4); + if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text'); + }); +}; diff --git a/src/json-crdt-extensions/peritext/block/__tests__/Blocks.refresh.spec.ts b/src/json-crdt-extensions/peritext/block/__tests__/Blocks.refresh.spec.ts new file mode 100644 index 0000000000..bc726ede8d --- /dev/null +++ b/src/json-crdt-extensions/peritext/block/__tests__/Blocks.refresh.spec.ts @@ -0,0 +1,70 @@ +import { + Kit, + setupAlphabetChunkSplitKit, + setupAlphabetKit, + setupAlphabetWithDeletesKit, + setupAlphabetWithTwoChunksKit, +} from '../../__tests__/setup'; + +const runTests = (setup: () => Kit) => { + test('updates block hash only where something was changed - leading block', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(10); + editor.saved.insMarker(['p'], 'p1'); + editor.cursor.setAt(22); + editor.saved.insMarker(['p'], 'p2'); + editor.cursor.setAt(editor.txt.str.length()); + peritext.refresh(); + const rootHash1 = peritext.blocks.root.hash; + const firstBlockHash1 = peritext.blocks.root.children[0].hash; + const secondBlockHash1 = peritext.blocks.root.children[1].hash; + editor.cursor.setAt(2); + editor.insert('___'); + peritext.refresh(); + const rootHash2 = peritext.blocks.root.hash; + const firstBlockHash2 = peritext.blocks.root.children[0].hash; + const secondBlockHash2 = peritext.blocks.root.children[1].hash; + expect(rootHash1).not.toBe(rootHash2); + expect(firstBlockHash1).not.toBe(firstBlockHash2); + expect(secondBlockHash1).toBe(secondBlockHash2); + }); + + test('updates block hash only where hash has changed - middle block', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(10); + editor.saved.insMarker(['p', 'p1']); + editor.cursor.setAt(22); + editor.saved.insMarker(['p'], 'p2'); + peritext.refresh(); + const rootHash1 = peritext.blocks.root.hash; + const firstBlockHash1 = peritext.blocks.root.children[0].hash; + const secondBlockHash1 = peritext.blocks.root.children[1].hash; + editor.cursor.setAt(13); + editor.insert('___'); + peritext.refresh(); + const rootHash2 = peritext.blocks.root.hash; + const firstBlockHash2 = peritext.blocks.root.children[0].hash; + const secondBlockHash2 = peritext.blocks.root.children[1].hash; + expect(rootHash1).not.toBe(rootHash2); + expect(firstBlockHash1).toBe(firstBlockHash2); + expect(secondBlockHash1).not.toBe(secondBlockHash2); + }); +}; + +describe('Blocks.refresh()', () => { + describe('basic alphabet', () => { + runTests(setupAlphabetKit); + }); + + describe('alphabet with two chunks', () => { + runTests(setupAlphabetWithTwoChunksKit); + }); + + describe('alphabet with chunk split', () => { + runTests(setupAlphabetChunkSplitKit); + }); + + describe('alphabet with deletes', () => { + runTests(setupAlphabetWithDeletesKit); + }); +}); diff --git a/src/json-crdt-extensions/peritext/block/__tests__/Inline.key.spec.ts b/src/json-crdt-extensions/peritext/block/__tests__/Inline.key.spec.ts index d4b6dcf7ca..e67b8f24c7 100644 --- a/src/json-crdt-extensions/peritext/block/__tests__/Inline.key.spec.ts +++ b/src/json-crdt-extensions/peritext/block/__tests__/Inline.key.spec.ts @@ -1,7 +1,15 @@ import {Timestamp} from '../../../../json-crdt-patch'; import {updateId} from '../../../../json-crdt/hash'; import {updateNum} from '../../../../json-hash'; -import {Kit, setupKit, setupNumbersKit, setupNumbersWithTombstonesKit} from '../../__tests__/setup'; +import { + Kit, + setupKit, + setupNumbersKit, + setupNumbersWithMultipleChunksAndDeletesKit, + setupNumbersWithRgaSplitKit, + setupNumbersWithTombstonesKit, + setupNumbersWithTwoChunksKit, +} from '../../__tests__/setup'; import {Point} from '../../rga/Point'; import {Inline} from '../Inline'; @@ -92,6 +100,18 @@ describe('Inline', () => { runKeyTests(setupNumbersWithTombstonesKit); }); + describe('numbers "0123456789", two RGA chunks', () => { + runKeyTests(setupNumbersWithTwoChunksKit); + }); + + describe('numbers "0123456789", with RGA split', () => { + runKeyTests(setupNumbersWithRgaSplitKit); + }); + + describe('numbers "0123456789", with multiple deletes', () => { + runKeyTests(setupNumbersWithMultipleChunksAndDeletesKit); + }); + describe('numbers "0123456789", with default schema and tombstones and constant sid', () => { runKeyTests(() => setupNumbersWithTombstonesKit(12313123)); }); diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts index b454f60ff5..3b6abc3890 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts @@ -1,4 +1,11 @@ -import {Kit, setupNumbersKit, setupNumbersWithTombstonesKit} from '../../__tests__/setup'; +import { + Kit, + setupNumbersKit, + setupNumbersWithMultipleChunksAndDeletesKit, + setupNumbersWithRgaSplitKit, + setupNumbersWithTombstonesKit, + setupNumbersWithTwoChunksKit, +} from '../../__tests__/setup'; import {MarkerOverlayPoint} from '../MarkerOverlayPoint'; const runMarkersTests = (setup: () => Kit) => { @@ -109,3 +116,15 @@ describe('numbers "0123456789", no edits', () => { describe('numbers "0123456789", with default schema and tombstones', () => { runMarkersTests(setupNumbersWithTombstonesKit); }); + +describe('numbers "0123456789", two RGA chunks', () => { + runMarkersTests(setupNumbersWithTwoChunksKit); +}); + +describe('numbers "0123456789", with RGA split', () => { + runMarkersTests(setupNumbersWithRgaSplitKit); +}); + +describe('numbers "0123456789", with multiple deletes', () => { + runMarkersTests(setupNumbersWithMultipleChunksAndDeletesKit); +}); diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.pairs.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.pairs.spec.ts index 5d5addca61..b5abb42d62 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.pairs.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.pairs.spec.ts @@ -1,5 +1,12 @@ import {next} from 'sonic-forest/lib/util'; -import {Kit, setupNumbersKit, setupNumbersWithTombstonesKit} from '../../__tests__/setup'; +import { + Kit, + setupNumbersKit, + setupNumbersWithMultipleChunksAndDeletesKit, + setupNumbersWithRgaSplitKit, + setupNumbersWithTombstonesKit, + setupNumbersWithTwoChunksKit, +} from '../../__tests__/setup'; import {Anchor} from '../../rga/constants'; import {MarkerOverlayPoint} from '../MarkerOverlayPoint'; import {OverlayPoint} from '../OverlayPoint'; @@ -162,3 +169,15 @@ describe('numbers "0123456789", no edits', () => { describe('numbers "0123456789", with default schema and tombstones', () => { runPairsTests(setupNumbersWithTombstonesKit); }); + +describe('numbers "0123456789", two RGA chunks', () => { + runPairsTests(setupNumbersWithTwoChunksKit); +}); + +describe('numbers "0123456789", with RGA split', () => { + runPairsTests(setupNumbersWithRgaSplitKit); +}); + +describe('numbers "0123456789", with multiple deletes', () => { + runPairsTests(setupNumbersWithMultipleChunksAndDeletesKit); +}); diff --git a/src/json-crdt-extensions/peritext/rga/__tests__/Range.text.spec.ts b/src/json-crdt-extensions/peritext/rga/__tests__/Range.text.spec.ts index 5373696a53..484f808b02 100644 --- a/src/json-crdt-extensions/peritext/rga/__tests__/Range.text.spec.ts +++ b/src/json-crdt-extensions/peritext/rga/__tests__/Range.text.spec.ts @@ -3,7 +3,10 @@ import { setupHelloWorldKit, setupHelloWorldWithFewEditsKit, setupNumbersKit, + setupNumbersWithMultipleChunksAndDeletesKit, + setupNumbersWithRgaSplitKit, setupNumbersWithTombstonesKit, + setupNumbersWithTwoChunksKit, } from '../../__tests__/setup'; const run = (setup: () => Kit) => { @@ -29,10 +32,22 @@ describe('some edits "hello world"', () => { run(setupHelloWorldWithFewEditsKit); }); -describe('no edits "number"', () => { +describe('numbers "0123456789", no edits', () => { run(setupNumbersKit); }); -describe('heavy edits "number"', () => { +describe('numbers "0123456789", with default schema and tombstones', () => { run(setupNumbersWithTombstonesKit); }); + +describe('numbers "0123456789", two RGA chunks', () => { + run(setupNumbersWithTwoChunksKit); +}); + +describe('numbers "0123456789", with RGA split', () => { + run(setupNumbersWithRgaSplitKit); +}); + +describe('numbers "0123456789", with multiple deletes', () => { + run(setupNumbersWithMultipleChunksAndDeletesKit); +});