diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.spec.ts new file mode 100644 index 0000000000..63e7e3dbf1 --- /dev/null +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.spec.ts @@ -0,0 +1,282 @@ +import {Model} from '../../../json-crdt/model'; +import {Peritext} from '../Peritext'; +import {Anchor} from '../rga/constants'; +import {Kit, setupHelloWorldKit, setupHelloWorldWithFewEditsKit} from './setup'; + +const run = (setup: () => Kit) => { + test('can run .refresh() on empty state', () => { + const model = Model.withLogicalClock(); + model.api.root({ + text: '', + slices: [], + }); + const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); + peritext.refresh(); + }); + + test('can insert a slice', () => { + const {peritext, model} = setup(); + peritext.editor.cursor.setAt(4, 5); + peritext.editor.saved.insMarker('bold', {bold: true}); + model.api.apply(); + const slices = model.s.text.toExt().slices().view(); + expect(slices).toMatchObject([[expect.any(Number), expect.any(Object), expect.any(Number), 'bold', {bold: true}]]); + }); + + describe('cursor', () => { + test('by default cursor is a collapsed caret', () => { + const {peritext} = setup(); + const text = peritext.editor.cursor.text(); + expect(text).toBe(''); + }); + + test('can select a local range and get text representation of it', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(0, 5); + const text = peritext.editor.cursor.text(); + expect(text).toBe('hello'); + }); + + test('can select first character', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(0, 1); + const text = peritext.editor.cursor.text(); + expect(text).toBe('h'); + }); + + test('can select second character', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(1, 1); + const text = peritext.editor.cursor.text(); + expect(text).toBe('e'); + }); + + test('can select character one before last', () => { + const {peritext, model} = setup(); + const text1 = (model.view() as any).text as string; + peritext.editor.cursor.setAt(text1.length - 2, 1); + const text2 = peritext.editor.cursor.text(); + expect(text2).toBe('l'); + }); + + test('can select last character', () => { + const {peritext, model} = setup(); + const text1 = (model.view() as any).text as string; + peritext.editor.cursor.setAt(text1.length - 1, 1); + const text2 = peritext.editor.cursor.text(); + expect(text2).toBe('d'); + }); + + test('can select the whole text', () => { + const {peritext, model} = setup(); + const text1 = (model.view() as any).text as string; + peritext.editor.cursor.setAt(0, text1.length); + const text2 = peritext.editor.cursor.text(); + expect(text2).toBe(text1); + }); + + test('can set an empty (caret) selection', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(1); + peritext.editor.insert('!'); + expect(peritext.str.view()).toBe('h!ello world'); + peritext.editor.cursor.setAt(1); + peritext.editor.insert('?'); + expect(peritext.str.view()).toBe('h?!ello world'); + peritext.editor.cursor.setAt(1); + peritext.editor.insert('+'); + expect(peritext.str.view()).toBe('h+?!ello world'); + peritext.editor.cursor.setAt(2); + peritext.editor.insert('GG'); + expect(peritext.str.view()).toBe('h+GG?!ello world'); + }); + + test('can set an empty (caret) selection at the end of the string', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(peritext.str.length()); + peritext.editor.insert('!'); + expect(peritext.str.view()).toBe('hello world!'); + peritext.editor.insert('?'); + expect(peritext.str.view()).toBe('hello world!?'); + peritext.editor.cursor.setAt(peritext.str.length() - 1); + peritext.editor.insert('+'); + expect(peritext.str.view()).toBe('hello world!+?'); + }); + }); + + describe('.collapseSelection()', () => { + test('does nothing when selection is already collapsed', () => { + const {peritext, model} = setup(); + const {editor} = peritext; + expect(editor.cursor.isCollapsed()).toBe(true); + editor.cursor.collapse(); + expect(editor.cursor.isCollapsed()).toBe(true); + expect((model.view() as any).text).toBe('hello world'); + }); + + test('removes text that was selected', () => { + const {peritext, model} = setup(); + const {editor} = peritext; + editor.cursor.setAt(2, 3); + expect(editor.cursor.isCollapsed()).toBe(false); + editor.cursor.collapse(); + expect(editor.cursor.isCollapsed()).toBe(true); + expect((model.view() as any).text).toBe('he world'); + }); + + test('can collapse at the beginning of string twice', () => { + const {peritext, model} = setup(); + const {editor} = peritext; + peritext.editor.cursor.setAt(0, 1); + expect(editor.cursor.isCollapsed()).toBe(false); + editor.cursor.collapse(); + expect(editor.cursor.isCollapsed()).toBe(true); + expect((model.view() as any).text).toBe('ello world'); + editor.cursor.setAt(0, 1); + expect(editor.cursor.isCollapsed()).toBe(false); + editor.cursor.collapse(); + expect(editor.cursor.isCollapsed()).toBe(true); + expect((model.view() as any).text).toBe('llo world'); + }); + + test('can collapse at the end of string twice', () => { + const {peritext, model} = setup(); + const {editor} = peritext; + editor.cursor.setAt(peritext.str.length() - 1, 1); + expect(editor.cursor.isCollapsed()).toBe(false); + editor.cursor.collapse(); + expect(editor.cursor.isCollapsed()).toBe(true); + expect((model.view() as any).text).toBe('hello worl'); + peritext.editor.cursor.setAt(peritext.str.length() - 1, 1); + expect(editor.cursor.isCollapsed()).toBe(false); + editor.cursor.collapse(); + expect(editor.cursor.isCollapsed()).toBe(true); + expect((model.view() as any).text).toBe('hello wor'); + }); + + test('can collapse the whole string', () => { + const {peritext, model} = setup(); + const {editor} = peritext; + editor.cursor.setAt(0, peritext.str.length()); + expect(editor.cursor.isCollapsed()).toBe(false); + editor.cursor.collapse(); + expect(editor.cursor.isCollapsed()).toBe(true); + expect((model.view() as any).text).toBe(''); + editor.insert('abc'); + expect((model.view() as any).text).toBe('abc'); + }); + }); + + describe('.nextId()', () => { + test('returns next char ID when cursor at string start', () => { + const {peritext, model} = setup(); + const {editor} = peritext; + expect(editor.cursor.start.id).toStrictEqual(peritext.str.id); + const nextId = editor.cursor.start.nextId()!; + editor.cursor.setAfter(nextId); + editor.insert('!'); + expect((model.view() as any).text).toBe('h!ello world'); + }); + + test('can walk all the way to string end', () => { + const {peritext, model} = setup(); + const {editor} = peritext; + expect(editor.cursor.start.id).toStrictEqual(peritext.str.id); + const nextId = editor.cursor.start.nextId()!; + editor.cursor.setAfter(nextId); + editor.insert('!'); + expect((model.view() as any).text).toBe('h!ello world'); + editor.insert('?'); + expect((model.view() as any).text).toBe('h!?ello world'); + editor.cursor.setAfter(editor.cursor.start.nextId()!); + editor.insert('.'); + expect((model.view() as any).text).toBe('h!?e.llo world'); + editor.cursor.setAfter(editor.cursor.start.nextId()!); + editor.cursor.setAfter(editor.cursor.start.nextId()!); + editor.cursor.setAfter(editor.cursor.start.nextId()!); + editor.cursor.setAfter(editor.cursor.start.nextId()!); + editor.cursor.setAfter(editor.cursor.start.nextId()!); + editor.insert('#'); + expect((model.view() as any).text).toBe('h!?e.llo w#orld'); + editor.cursor.setAfter(editor.cursor.start.nextId()!); + editor.cursor.setAfter(editor.cursor.start.nextId()!); + editor.cursor.setAfter(editor.cursor.start.nextId()!); + editor.cursor.setAfter(editor.cursor.start.nextId()!); + editor.insert('+'); + expect((model.view() as any).text).toBe('h!?e.llo w#orld+'); + }); + }); + + describe('.insert()', () => { + test('can insert at caret position', () => { + const {peritext, model} = setup(); + const {editor} = peritext; + editor.insert('H'); + expect((model.view() as any).text).toBe('Hhello world'); + }); + + test('can insert text in when cursor is range', () => { + const {peritext, model} = setup(); + const {editor} = peritext; + const firstCharId = peritext.str.find(0)!; + editor.cursor.set(peritext.point(firstCharId, Anchor.Before), peritext.point(firstCharId, Anchor.After)); + editor.insert('H'); + expect((model.view() as any).text).toBe('Hello world'); + }); + }); + + describe('deletions', () => { + test('does nothing when deleting at the start of a string', () => { + const {peritext} = setup(); + const {editor} = peritext; + editor.delBwd(); + expect(peritext.str.view()).toBe('hello world'); + }); + + test('can delete one character at the beginning of a string', () => { + const {peritext} = setup(); + const {editor} = peritext; + editor.cursor.setAt(1); + editor.delBwd(); + expect(peritext.str.view()).toBe('ello world'); + editor.delBwd(); + expect(peritext.str.view()).toBe('ello world'); + editor.delBwd(); + expect(peritext.str.view()).toBe('ello world'); + }); + + test('can delete two characters at the beginning of a string', () => { + const {peritext} = setup(); + const {editor} = peritext; + editor.cursor.setAt(2); + editor.delBwd(); + expect(peritext.str.view()).toBe('hllo world'); + editor.delBwd(); + expect(peritext.str.view()).toBe('llo world'); + editor.delBwd(); + expect(peritext.str.view()).toBe('llo world'); + }); + + test('can delete a range selection', () => { + const {peritext} = setup(); + const {editor} = peritext; + editor.cursor.setAt(2, 3); + editor.delBwd(); + expect(peritext.str.view()).toBe('he world'); + editor.delBwd(); + expect(peritext.str.view()).toBe('h world'); + editor.delBwd(); + expect(peritext.str.view()).toBe(' world'); + editor.delBwd(); + expect(peritext.str.view()).toBe(' world'); + }); + }); +}; + +describe('no edits "hello world"', () => { + run(setupHelloWorldKit); +}); + +describe('some edits "hello world"', () => { + run(setupHelloWorldWithFewEditsKit); +}); diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.tree.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.tree.spec.ts new file mode 100644 index 0000000000..b95a0f7eb4 --- /dev/null +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.tree.spec.ts @@ -0,0 +1,297 @@ +import {InlineAttrPos} from '../block/Inline'; +import {LeafBlock} from '../block/LeafBlock'; +import {CursorAnchor, SliceTypes} from '../slice/constants'; +import {Kit, setupHelloWorldKit, setupHelloWorldWithFewEditsKit} from './setup'; + +const run = (setup: () => Kit) => { + describe('inline', () => { + test('creates two inline elements by annotating a slice at the beginning of the text', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(0, 5); + peritext.editor.saved.insOverwrite(123); + peritext.editor.cursor.setAt(peritext.str.length()); + peritext.editor.delCursors(); + peritext.refresh(); + const leaf = peritext.blocks.root.children[0] as LeafBlock; + expect(leaf.text()).toBe('hello world'); + const inline1 = [...leaf.texts()].find((inline) => inline.text() === 'hello')!; + const inline2 = [...leaf.texts()].find((inline) => inline.text() === ' world')!; + expect(inline1.text()).toBe('hello'); + expect(inline2.text()).toBe(' world'); + expect(inline1.attr()).toEqual({123: [1, InlineAttrPos.Contained]}); + expect(inline2.attr()).toEqual({}); + }); + + test('creates two inline elements by annotating a slice at the end of the text', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(6, 5); + peritext.editor.saved.insOverwrite(123); + peritext.editor.cursor.setAt(peritext.str.length()); + peritext.refresh(); + const leaf = peritext.blocks.root.children[0] as LeafBlock; + const inline1 = [...leaf.texts()].find((inline) => inline.text() === 'hello ')!; + const inline2 = [...leaf.texts()].find((inline) => inline.text() === 'world')!; + expect(inline1.text()).toBe('hello '); + expect(inline2.text()).toBe('world'); + expect(inline1.attr()).toEqual({}); + expect(inline2.attr()).toEqual({123: [1, InlineAttrPos.Contained]}); + }); + + test('creates three inline elements by annotating a slice in the middle of the text', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3, 3); + peritext.editor.saved.insOverwrite('type', {foo: 'bar'}); + peritext.editor.cursor.setAt(peritext.str.length()); + peritext.refresh(); + const leaf = peritext.blocks.root.children[0] as LeafBlock; + const inline1 = [...leaf.texts()].find((inline) => inline.text() === 'hel')!; + const inline2 = [...leaf.texts()].find((inline) => inline.text() === 'lo ')!; + const inline3 = [...leaf.texts()].find((inline) => inline.text() === 'world')!; + expect(inline1.text()).toBe('hel'); + expect(inline2.text()).toBe('lo '); + expect(inline3.text()).toBe('world'); + expect(inline1.attr()).toEqual({}); + expect(inline2.attr()).toEqual({type: [{foo: 'bar'}, InlineAttrPos.Contained]}); + expect(inline3.attr()).toEqual({}); + }); + + describe('two interleaving annotations', () => { + test('partial slices result in 5 inline nodes', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3, 4); + peritext.editor.saved.insStack('bold'); + peritext.editor.cursor.setAt(5, 4); + peritext.editor.saved.insStack('italic'); + peritext.refresh(); + const leaf = peritext.blocks.root.children[0]; + const inline1 = [...leaf.texts()].find((inline) => inline.text() === 'hel')!; + const inline2 = [...leaf.texts()].find((inline) => inline.text() === 'lo')!; + const inline3 = [...leaf.texts()].find((inline) => inline.text() === ' w')!; + const inline4 = [...leaf.texts()].find((inline) => inline.text() === 'or')!; + const inline5 = [...leaf.texts()].find((inline) => inline.text() === 'ld')!; + expect(leaf.text()).toBe('hello world'); + expect(inline1.attr()).toEqual({}); + expect(inline2.attr()).toEqual({bold: [[void 0], InlineAttrPos.Start]}); + expect(inline3.attr()).toEqual({ + [SliceTypes.Cursor]: [[[CursorAnchor.Start, void 0]], InlineAttrPos.Start], + bold: [[void 0], InlineAttrPos.End], + italic: [[void 0], InlineAttrPos.Start], + }); + expect(inline4.attr()).toEqual({ + [SliceTypes.Cursor]: [[[CursorAnchor.Start, void 0]], InlineAttrPos.End], + italic: [[void 0], InlineAttrPos.End], + }); + expect(inline5.attr()).toEqual({}); + }); + + test('two completely overlaying slices result in 3 inline nodes', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3, 4); + peritext.editor.saved.insStack('bold'); + peritext.editor.saved.insStack('italic'); + peritext.refresh(); + const leaf = peritext.blocks.root.children[0]; + expect(leaf.text()).toBe('hello world'); + const inline1 = [...leaf.texts()].find((inline) => inline.text() === 'hel')!; + const inline2 = [...leaf.texts()].find((inline) => inline.text() === 'lo w')!; + const inline3 = [...leaf.texts()].find((inline) => inline.text() === 'orld')!; + expect(inline1.attr()).toEqual({}); + expect(inline2.attr()).toEqual({ + [SliceTypes.Cursor]: [[[CursorAnchor.Start, void 0]], InlineAttrPos.Contained], + italic: [[void 0], InlineAttrPos.Contained], + bold: [[void 0], InlineAttrPos.Contained], + }); + expect(inline3.attr()).toEqual({}); + }); + + test('two slices with the same start result in 4 inline nodes', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3, 4); + peritext.editor.saved.insStack('bold'); + peritext.editor.cursor.setAt(3, 5); + peritext.editor.saved.insStack('italic'); + peritext.refresh(); + const leaf = peritext.blocks.root.children[0]; + expect(leaf.text()).toBe('hello world'); + const inline1 = [...leaf.texts()].find((inline) => inline.text() === 'hel')!; + const inline2 = [...leaf.texts()].find((inline) => inline.text() === 'lo w')!; + const inline3 = [...leaf.texts()].find((inline) => inline.text() === 'o')!; + const inline4 = [...leaf.texts()].find((inline) => inline.text() === 'rld')!; + expect(inline1.attr()).toEqual({}); + expect(inline2.attr()).toEqual({ + [SliceTypes.Cursor]: [[[CursorAnchor.Start, void 0]], InlineAttrPos.Start], + italic: [[void 0], InlineAttrPos.Start], + bold: [[void 0], InlineAttrPos.Contained], + }); + expect(inline3.attr()).toEqual({ + [SliceTypes.Cursor]: [[[CursorAnchor.Start, void 0]], InlineAttrPos.End], + italic: [[void 0], InlineAttrPos.End], + }); + expect(inline4.attr()).toEqual({}); + }); + + test('two slices with the same end result in 4 inline nodes', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3, 4); + peritext.editor.saved.insStack('bold'); + peritext.editor.cursor.setAt(4, 3); + peritext.editor.saved.insStack('italic'); + peritext.refresh(); + const leaf = peritext.blocks.root.children[0]; + expect(leaf.text()).toBe('hello world'); + const inline1 = [...leaf.texts()].find((inline) => inline.text() === 'hel')!; + const inline2 = [...leaf.texts()].find((inline) => inline.text() === 'l')!; + const inline3 = [...leaf.texts()].find((inline) => inline.text() === 'o w')!; + const inline4 = [...leaf.texts()].find((inline) => inline.text() === 'orld')!; + expect(inline1.attr()).toEqual({}); + expect(inline2.attr()).toEqual({ + bold: [[void 0], InlineAttrPos.Start], + }); + expect(inline3.attr()).toEqual({ + [SliceTypes.Cursor]: [[[CursorAnchor.Start, void 0]], InlineAttrPos.Contained], + italic: [[void 0], InlineAttrPos.Contained], + bold: [[void 0], InlineAttrPos.End], + }); + expect(inline4.attr()).toEqual({}); + }); + + test('two adjacent slices 4 inline nodes', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3, 2); + peritext.editor.saved.insStack('bold'); + peritext.editor.cursor.setAt(5, 3); + peritext.editor.saved.insStack('italic'); + peritext.refresh(); + const leaf = peritext.blocks.root.children[0]; + expect(leaf.text()).toBe('hello world'); + const inline1 = [...leaf.texts()].find((inline) => inline.text() === 'hel')!; + const inline2 = [...leaf.texts()].find((inline) => inline.text() === 'lo')!; + const inline3 = [...leaf.texts()].find((inline) => inline.text() === ' wo')!; + const inline4 = [...leaf.texts()].find((inline) => inline.text() === 'rld')!; + expect(inline1.attr()).toEqual({}); + expect(inline2.attr()).toEqual({ + bold: [[void 0], InlineAttrPos.Contained], + }); + expect(inline3.attr()).toEqual({ + [SliceTypes.Cursor]: [[[CursorAnchor.Start, void 0]], InlineAttrPos.Contained], + italic: [[void 0], InlineAttrPos.Contained], + }); + expect(inline4.attr()).toEqual({}); + }); + }); + }); + + describe('block', () => { + describe('with no overlays', () => { + test('first block captures all text', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3); + peritext.refresh(); + expect(peritext.blocks.root.children[0].text()).toBe('hello world'); + }); + }); + + test('two blocks capture the text', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3); + peritext.editor.saved.insMarker(['p'], '¶'); + peritext.refresh(); + const [block1, block2] = peritext.blocks.root.children; + expect(block1.text()).toBe('hel'); + expect(block2.text()).toBe('lo world'); + }); + + test('three blocks capture the text', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3); + peritext.editor.saved.insMarker(['p'], '¶'); + peritext.editor.cursor.setAt(8); + peritext.editor.saved.insMarker(['blockquote'], {url: 'http:/...'}); + peritext.refresh(); + const [block1, block2, block3] = peritext.blocks.root.children; + expect(block1.text()).toBe('hel'); + expect([...block1.texts()].length).toBe(1); + expect([...block1.texts()][0].text()).toBe('hel'); + expect(block2.text()).toBe('lo w'); + expect([...block2.texts()].length).toBe(2); + expect([...block2.texts()][0].text()).toBe('lo w'); + expect(block3.text()).toBe('orld'); + expect([...block3.texts()].length).toBe(1); + expect([...block3.texts()][0].text()).toBe('orld'); + }); + + test('slice each character into its own paragraph', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(1); + peritext.editor.saved.insMarker(['p']); + peritext.editor.cursor.setAt(3); + peritext.editor.saved.insMarker(['p']); + peritext.editor.cursor.setAt(5); + peritext.editor.saved.insMarker(['p']); + peritext.refresh(); + expect(peritext.blocks.root.children[0].text()).toBe('h'); + expect(peritext.blocks.root.children[1].text()).toBe('e'); + expect(peritext.blocks.root.children[2].text()).toBe('l'); + expect(peritext.blocks.root.children[3].text()).toBe('lo world'); + }); + + test('inline slice across paragraph blocks works', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3, 4); + peritext.editor.saved.insStack('bold'); + peritext.editor.cursor.setAt(5); + peritext.editor.saved.insMarker('p', {important: true}); + peritext.refresh(); + const block1 = peritext.blocks.root.children[0]; + const block2 = peritext.blocks.root.children[1]; + expect([...block1.texts()].length).toBe(3); + expect([...block1.texts()][0].attr()).toEqual({}); + expect([...block1.texts()][1].attr()).toEqual({bold: [[void 0], InlineAttrPos.Start]}); + expect([...block2.texts()].length).toBe(2); + expect([...block2.texts()][0].attr()).toEqual({bold: [[void 0], InlineAttrPos.End]}); + expect([...block2.texts()][1].attr()).toEqual({}); + }); + + test('inline slice contains a whole block', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3); + peritext.editor.saved.insMarker(['p'], {}); + peritext.editor.cursor.setAt(6); + peritext.editor.saved.insMarker(['p'], {}); + peritext.editor.cursor.setAt(2, 9); + peritext.editor.saved.insStack('bold'); + peritext.refresh(); + const block1 = peritext.blocks.root.children[0]; + const block2 = peritext.blocks.root.children[1]; + const block3 = peritext.blocks.root.children[2]; + expect(block1.text()).toBe('hel'); + expect(block2.text()).toBe('lo'); + expect(block3.text()).toBe(' world'); + expect([...block1.texts()].length).toBe(2); + expect([...block1.texts()][0].attr()).toEqual({}); + expect([...block1.texts()][1].attr()).toEqual({ + [SliceTypes.Cursor]: [[[CursorAnchor.Start, void 0]], InlineAttrPos.Start], + bold: [[void 0], InlineAttrPos.Start], + }); + expect([...block2.texts()].length).toBe(1); + expect([...block2.texts()][0].attr()).toEqual({ + [SliceTypes.Cursor]: [[[CursorAnchor.Start, void 0]], InlineAttrPos.Passing], + bold: [[void 0], InlineAttrPos.Passing], + }); + expect([...block3.texts()].length).toBe(2); + expect([...block3.texts()][0].attr()).toEqual({ + [SliceTypes.Cursor]: [[[CursorAnchor.Start, void 0]], InlineAttrPos.End], + bold: [[void 0], InlineAttrPos.End], + }); + expect([...block3.texts()][1].attr()).toEqual({}); + }); + }); +}; + +describe('no edits "hello world"', () => { + run(setupHelloWorldKit); +}); + +describe('some edits "hello world"', () => { + run(setupHelloWorldWithFewEditsKit); +});