Skip to content

Commit 9be59fe

Browse files
committed
test(json-crdt-extensions): 💍 improve Range tests
1 parent d958086 commit 9be59fe

File tree

4 files changed

+177
-33
lines changed

4 files changed

+177
-33
lines changed

src/json-crdt-extensions/peritext/Peritext.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export class Peritext implements Printable {
2828
this.editor = new Editor(this);
2929
}
3030

31+
public strApi() {
32+
return this.model.api.wrap(this.str);
33+
}
34+
3135
// ------------------------------------------------------------------- Points
3236

3337
/**
@@ -42,9 +46,10 @@ export class Peritext implements Printable {
4246
}
4347

4448
/**
45-
* Creates a point at a view position in the text.
49+
* Creates a point at a view position in the text. The `pos` argument specifies
50+
* the position of the character, not the gap between characters.
4651
*
47-
* @param pos View position in the text.
52+
* @param pos Position of the character in the text.
4853
* @param anchor Whether the point should attach before or after a character.
4954
* @returns The point.
5055
*/
@@ -131,7 +136,7 @@ export class Peritext implements Printable {
131136
* @param text Text to insert.
132137
*/
133138
public insAt(pos: number, text: string): void {
134-
const str = this.model.api.wrap(this.str);
139+
const str = this.strApi();
135140
str.ins(pos, text);
136141
}
137142

src/json-crdt-extensions/peritext/slice/Cursor.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export class Cursor extends Range implements Slice {
1515
* the end which does not move when user changes selection. The other
1616
* end is free to move, the moving end of the cursor is "focus". By default
1717
* "anchor" is the start of the cursor.
18+
*
19+
* @todo Create a custom enum for this, instead of using `Anchor`.
1820
*/
1921
public base: Anchor = Anchor.Before;
2022

@@ -35,10 +37,10 @@ export class Cursor extends Range implements Slice {
3537
return this.base === Anchor.Before ? this.end : this.start;
3638
}
3739

38-
public set(start: Point, end?: Point, anchor: Anchor = Anchor.Before): void {
40+
public set(start: Point, end?: Point, base: Anchor = Anchor.Before): void {
3941
if (!end || end === start) end = start.clone();
4042
super.set(start, end);
41-
this.base = anchor;
43+
this.base = base;
4244
}
4345

4446
public setAt(start: number, length: number = 0): void {
@@ -75,7 +77,7 @@ export class Cursor extends Range implements Slice {
7577
}
7678
}
7779

78-
/** @deprecated What is this method for? */
80+
/** @todo Maybe move it to another interface? */
7981
public del(): boolean {
8082
return false;
8183
}

src/json-crdt-extensions/peritext/slice/Range.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ export class Range implements Printable {
5959
const pos2 = end.pos();
6060
if (pos1 === pos2) {
6161
if (start.anchor === end.anchor) return true;
62-
// TODO: inspect below cases, if they are needed
6362
if (start.anchor === Anchor.After) return true;
6463
else {
6564
const chunk = start.chunk();
6665
if (chunk && chunk.del) {
67-
this.start = this.end.clone();
66+
// TODO: Revisit where is the best place for this normalization.
67+
// this.start = this.end.clone();
6868
return true;
6969
}
7070
}

src/json-crdt-extensions/peritext/slice/__tests__/Range.spec.ts

Lines changed: 162 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,159 @@ import {Peritext} from '../../Peritext';
33
import {Anchor} from '../../constants';
44
import {Editor} from '../../editor/Editor';
55

6-
const setup = (insert: (editor: Editor) => void = (editor) => editor.insert('Hello world!')) => {
6+
const setup = (insert: (peritext: Peritext) => void = (peritext) => peritext.strApi().ins(0, 'Hello world!')) => {
77
const model = Model.withLogicalClock();
88
model.api.root({
99
text: '',
1010
slices: [],
1111
});
1212
const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node);
13-
const editor = peritext.editor;
14-
insert(editor);
15-
return {model, peritext, editor};
13+
insert(peritext);
14+
return {model, peritext};
1615
};
1716

17+
const setupEvenDeleted = () => {
18+
return setup((peritext) => {
19+
peritext.strApi().ins(0, '0123456789');
20+
peritext.strApi().del(0, 1);
21+
peritext.strApi().del(1, 1);
22+
peritext.strApi().del(2, 1);
23+
peritext.strApi().del(3, 1);
24+
peritext.strApi().del(4, 1);
25+
});
26+
};
27+
28+
describe('new', () => {
29+
test('creates a range from two points', () => {
30+
const {peritext} = setup();
31+
const range = peritext.rangeAt(1, 2);
32+
expect(range.text()).toBe('el');
33+
expect(range.start.pos()).toBe(1);
34+
expect(range.start.viewPos()).toBe(1);
35+
expect(range.start.anchor).toBe(Anchor.Before);
36+
expect(range.end.pos()).toBe(2);
37+
expect(range.end.viewPos()).toBe(3);
38+
expect(range.end.anchor).toBe(Anchor.After);
39+
});
40+
});
41+
42+
describe('.from()', () => {
43+
test('creates a when two points are in reverse order', () => {
44+
const {peritext} = setup();
45+
const rangeTmp = peritext.rangeAt(1, 2);
46+
const range = peritext.rangeFromPoints(rangeTmp.end, rangeTmp.start);
47+
expect(range.text()).toBe('el');
48+
expect(range.start.pos()).toBe(1);
49+
expect(range.start.viewPos()).toBe(1);
50+
expect(range.start.anchor).toBe(Anchor.Before);
51+
expect(range.end.pos()).toBe(2);
52+
expect(range.end.viewPos()).toBe(3);
53+
expect(range.end.anchor).toBe(Anchor.After);
54+
});
55+
});
56+
57+
describe('.clone()', () => {
58+
test('can clone a range', () => {
59+
const {peritext} = setup();
60+
const range1 = peritext.rangeAt(2, 3);
61+
const range2 = range1.clone();
62+
expect(range2).not.toBe(range1);
63+
expect(range1.text()).toBe(range2.text());
64+
expect(range2.start).not.toBe(range1.start);
65+
expect(range2.end).not.toBe(range1.end);
66+
expect(range2.start.refresh()).toBe(range1.start.refresh());
67+
expect(range2.end.refresh()).toBe(range1.end.refresh());
68+
expect(range2.start.compare(range1.start)).toBe(0);
69+
expect(range2.end.compare(range1.end)).toBe(0);
70+
});
71+
});
72+
73+
describe('.isCollapsed()', () => {
74+
describe('when range is collapsed', () => {
75+
test('returns true at the beginning of string', () => {
76+
const {peritext} = setup();
77+
const point = peritext.pointAtStart();
78+
const range = peritext.range(point, point);
79+
const isCollapsed = range.isCollapsed();
80+
expect(isCollapsed).toBe(true);
81+
});
82+
83+
test('returns true at the end of string', () => {
84+
const {peritext} = setup();
85+
const point = peritext.pointAtEnd();
86+
const range = peritext.range(point, point);
87+
const isCollapsed = range.isCollapsed();
88+
expect(isCollapsed).toBe(true);
89+
});
90+
91+
test('returns true when before first character', () => {
92+
const {peritext} = setup();
93+
const point = peritext.pointAt(0, Anchor.Before);
94+
const range = peritext.range(point, point);
95+
const isCollapsed = range.isCollapsed();
96+
expect(isCollapsed).toBe(true);
97+
});
98+
99+
test('returns true when after last character', () => {
100+
const {peritext} = setup();
101+
const point = peritext.pointAt(peritext.str.length() - 1, Anchor.After);
102+
const range = peritext.range(point, point);
103+
const isCollapsed = range.isCollapsed();
104+
expect(isCollapsed).toBe(true);
105+
});
106+
107+
test('returns true when in the middle of plain/undeleted text', () => {
108+
const {peritext} = setup();
109+
const point1 = peritext.pointAt(2, Anchor.After);
110+
const point2 = peritext.pointAt(3, Anchor.Before);
111+
const range1 = peritext.range(point1, point1);
112+
const range2 = peritext.range(point2, point2);
113+
expect(range1.isCollapsed()).toBe(true);
114+
expect(range2.isCollapsed()).toBe(true);
115+
});
116+
117+
describe('when first character is deleted', () => {
118+
test('returns true at the beginning of string', () => {
119+
const {peritext} = setupEvenDeleted();
120+
const point = peritext.pointAtStart();
121+
const range = peritext.range(point, point);
122+
const isCollapsed = range.isCollapsed();
123+
expect(isCollapsed).toBe(true);
124+
});
125+
126+
test('returns true when before first character', () => {
127+
const {peritext} = setupEvenDeleted();
128+
const point = peritext.pointAt(0, Anchor.Before);
129+
const range = peritext.range(point, point);
130+
const isCollapsed = range.isCollapsed();
131+
expect(isCollapsed).toBe(true);
132+
});
133+
134+
test('returns true when in the middle of deleted characters', () => {
135+
const {peritext} = setupEvenDeleted();
136+
const range = peritext.rangeAt(2, 1);
137+
expect(range.isCollapsed()).toBe(false);
138+
peritext.strApi().del(1, 3);
139+
expect(range.isCollapsed()).toBe(true);
140+
});
141+
142+
test('returns true when whole text was deleted', () => {
143+
const {peritext} = setupEvenDeleted();
144+
const range = peritext.rangeAt(1, 3);
145+
expect(range.isCollapsed()).toBe(false);
146+
peritext.strApi().del(0, 5);
147+
expect(range.isCollapsed()).toBe(true);
148+
});
149+
});
150+
});
151+
});
152+
18153
describe('.contains()', () => {
19154
test('returns true if slice is contained', () => {
20-
const {peritext, editor} = setup();
21-
editor.setCursor(3, 2);
22-
const slice = editor.insertOverwriteSlice('b');
23-
editor.setCursor(0);
155+
const {peritext} = setup();
156+
peritext.editor.setCursor(3, 2);
157+
const slice = peritext.editor.insertOverwriteSlice('b');
158+
peritext.editor.setCursor(0);
24159
peritext.refresh();
25160
expect(peritext.rangeAt(2, 4).contains(slice)).toBe(true);
26161
expect(peritext.rangeAt(3, 4).contains(slice)).toBe(true);
@@ -29,10 +164,10 @@ describe('.contains()', () => {
29164
});
30165

31166
test('returns false if slice is not contained', () => {
32-
const {peritext, editor} = setup();
33-
editor.setCursor(3, 2);
34-
const slice = editor.insertOverwriteSlice('b');
35-
editor.setCursor(0);
167+
const {peritext} = setup();
168+
peritext.editor.setCursor(3, 2);
169+
const slice = peritext.editor.insertOverwriteSlice('b');
170+
peritext.editor.setCursor(0);
36171
peritext.refresh();
37172
expect(peritext.rangeAt(3, 1).contains(slice)).toBe(false);
38173
expect(peritext.rangeAt(2, 1).contains(slice)).toBe(false);
@@ -45,24 +180,25 @@ describe('.contains()', () => {
45180

46181
describe('.isCollapsed()', () => {
47182
test('returns true when endpoints point to the same location', () => {
48-
const {editor} = setup();
49-
editor.setCursor(3);
50-
expect(editor.cursor.isCollapsed()).toBe(true);
183+
const {peritext} = setup();
184+
peritext.editor.setCursor(3);
185+
expect(peritext.editor.cursor.isCollapsed()).toBe(true);
51186
});
52187

53188
test('returns true when when there is no visible content between endpoints', () => {
54-
const {peritext, editor} = setup();
189+
const {peritext} = setup();
55190
const range = peritext.rangeAt(2, 1);
56-
editor.setCursor(2, 1);
57-
editor.delete();
191+
peritext.editor.setCursor(2, 1);
192+
peritext.editor.delete();
58193
expect(range.isCollapsed()).toBe(true);
59194
});
60195
});
61196

62197
describe('.expand()', () => {
63198
const runExpandTests = (setup2: typeof setup) => {
64199
test('can expand anchors to include adjacent elements', () => {
65-
const {editor} = setup2();
200+
const {peritext} = setup2();
201+
const editor = peritext.editor;
66202
editor.setCursor(1, 1);
67203
expect(editor.cursor.start.pos()).toBe(1);
68204
expect(editor.cursor.start.anchor).toBe(Anchor.Before);
@@ -77,15 +213,15 @@ describe('.expand()', () => {
77213
});
78214

79215
test('can expand anchors to contain include adjacent tombstones', () => {
80-
const {peritext, editor} = setup2();
216+
const {peritext} = setup2();
81217
const tombstone1 = peritext.rangeAt(1, 1);
82218
tombstone1.expand();
83219
const tombstone2 = peritext.rangeAt(3, 1);
84220
tombstone2.expand();
85-
editor.cursor.setRange(tombstone1);
86-
editor.delete();
87-
editor.cursor.setRange(tombstone2);
88-
editor.delete();
221+
peritext.editor.cursor.setRange(tombstone1);
222+
peritext.editor.delete();
223+
peritext.editor.cursor.setRange(tombstone2);
224+
peritext.editor.delete();
89225
const range = peritext.rangeAt(1, 1);
90226
range.expand();
91227
expect(range.start.pos()).toBe(tombstone1.start.pos());
@@ -101,7 +237,8 @@ describe('.expand()', () => {
101237

102238
describe('each car is own chunk', () => {
103239
runExpandTests(() =>
104-
setup((editor) => {
240+
setup((peritext) => {
241+
const editor = peritext.editor;
105242
editor.insert('!');
106243
editor.setCursor(0);
107244
editor.insert('d');

0 commit comments

Comments
 (0)