Skip to content

Commit b1be984

Browse files
committed
test(json-crdt-extensions): đź’Ť hand edge cases in .leftChar() and .rightChar()
1 parent ab88fae commit b1be984

File tree

2 files changed

+110
-90
lines changed

2 files changed

+110
-90
lines changed

‎src/json-crdt-extensions/peritext/point/Point.ts

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,33 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
198198
return;
199199
}
200200

201+
public leftChar(): ChunkSlice | undefined {
202+
const str = this.txt.str;
203+
if (this.isEndOfStr()) {
204+
let chunk = str.last();
205+
while (chunk && chunk.del) chunk = str.prev(chunk);
206+
return chunk ? new ChunkSlice(chunk, chunk.span - 1, 1) : undefined;
207+
}
208+
let chunk = this.chunk();
209+
if (!chunk) return;
210+
if (chunk.del) {
211+
const prevId = this.prevId();
212+
if (!prevId) return;
213+
const tmp = new Point(this.txt, prevId, Anchor.After);
214+
return tmp.leftChar();
215+
}
216+
if (this.anchor === Anchor.After) {
217+
const off = this.id.time - chunk.id.time;
218+
return new ChunkSlice(chunk, off, 1);
219+
}
220+
const off = this.id.time - chunk.id.time - 1;
221+
if (off >= 0) return new ChunkSlice(chunk, off, 1);
222+
chunk = str.prev(chunk);
223+
while (chunk && chunk.del) chunk = str.prev(chunk);
224+
if (!chunk) return;
225+
return new ChunkSlice(chunk, chunk.span - 1, 1);
226+
}
227+
201228
public rightChar(): ChunkSlice | undefined {
202229
const str = this.txt.str;
203230
if (this.isStartOfStr()) {
@@ -225,29 +252,6 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
225252
return new ChunkSlice(chunk, 0, 1);
226253
}
227254

228-
public leftChar(): ChunkSlice | undefined {
229-
let chunk = this.chunk();
230-
// TODO: Handle case when point references end of str.
231-
if (!chunk) return;
232-
if (chunk.del) {
233-
const prevId = this.prevId();
234-
if (!prevId) return;
235-
const tmp = new Point(this.txt, prevId, Anchor.After);
236-
return tmp.leftChar();
237-
}
238-
if (this.anchor === Anchor.After) {
239-
const off = this.id.time - chunk.id.time;
240-
return new ChunkSlice(chunk, off, 1);
241-
}
242-
const off = this.id.time - chunk.id.time - 1;
243-
if (off >= 0) return new ChunkSlice(chunk, off, 1);
244-
const str = this.txt.str;
245-
chunk = str.prev(chunk);
246-
while (chunk && chunk.del) chunk = str.prev(chunk);
247-
if (!chunk) return;
248-
return new ChunkSlice(chunk, chunk.span - 1, 1);
249-
}
250-
251255
public isStartOfStr(): boolean {
252256
return equal(this.id, this.txt.str.id) && this.anchor === Anchor.After;
253257
}

‎src/json-crdt-extensions/peritext/point/__tests__/Point.spec.ts

Lines changed: 83 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -563,73 +563,6 @@ describe('.prevId()', () => {
563563
});
564564
});
565565

566-
describe('.rightChar()', () => {
567-
test('returns the right character', () => {
568-
const model = Model.withLogicalClock(123456);
569-
model.api.root({
570-
text: 'abc',
571-
slices: [],
572-
});
573-
const str = model.api.str(['text']).node;
574-
const peritext = new Peritext(model, str, model.api.arr(['slices']).node);
575-
const point0 = peritext.pointAt(0);
576-
const char0 = point0.rightChar()!;
577-
expect(char0.chunk.data!.slice(char0.off, char0.off + 1)).toBe('a');
578-
const point1 = peritext.pointAt(1);
579-
const char1 = point1.rightChar()!;
580-
expect(char1.chunk.data!.slice(char1.off, char1.off + 1)).toBe('b');
581-
const point2 = peritext.pointAt(2);
582-
const char2 = point2.rightChar()!;
583-
expect(char2.chunk.data!.slice(char2.off, char2.off + 1)).toBe('c');
584-
});
585-
586-
test('multi-char chunks with deletes', () => {
587-
const {peritext} = setupWithText();
588-
const res = '012345678';
589-
for (let i = 0; i < res.length; i++) {
590-
const point = peritext.pointAt(i, Anchor.Before);
591-
const char = point.rightChar()!;
592-
expect(char.view()).toBe(res[i]);
593-
}
594-
for (let i = 0; i < res.length - 1; i++) {
595-
const point = peritext.pointAt(i, Anchor.After);
596-
const char = point.rightChar()!;
597-
expect(char.view()).toBe(res[i + 1]);
598-
}
599-
const end = peritext.pointAt(res.length - 1, Anchor.After);
600-
expect(end.rightChar()).toBe(undefined);
601-
});
602-
603-
test('multi-char chunks with deletes (2)', () => {
604-
const {peritext} = setupWithChunkedText();
605-
const res = '123456789';
606-
for (let i = 0; i < res.length; i++) {
607-
const point = peritext.pointAt(i, Anchor.Before);
608-
const char = point.rightChar()!;
609-
expect(char.view()).toBe(res[i]);
610-
}
611-
for (let i = 0; i < res.length - 1; i++) {
612-
const point = peritext.pointAt(i, Anchor.After);
613-
const char = point.rightChar()!;
614-
expect(char.view()).toBe(res[i + 1]);
615-
}
616-
const end = peritext.pointAt(res.length - 1, Anchor.After);
617-
expect(end.rightChar()).toBe(undefined);
618-
});
619-
620-
test('retrieves right char of a deleted point', () => {
621-
const {peritext, chunkD1, chunkD2} = setupWithChunkedText();
622-
const p1 = peritext.point(chunkD1.id, Anchor.Before);
623-
expect(p1.rightChar()!.view()).toBe('4');
624-
const p2 = peritext.point(chunkD1.id, Anchor.After);
625-
expect(p2.rightChar()!.view()).toBe('4');
626-
const p3 = peritext.point(chunkD2.id, Anchor.Before);
627-
expect(p3.rightChar()!.view()).toBe('7');
628-
const p4 = peritext.point(chunkD2.id, Anchor.After);
629-
expect(p4.rightChar()!.view()).toBe('7');
630-
});
631-
});
632-
633566
describe('.leftChar()', () => {
634567
test('returns the left character', () => {
635568
const model = Model.withLogicalClock(123456);
@@ -711,6 +644,89 @@ describe('.leftChar()', () => {
711644
const p4 = peritext.point(chunkD2.id, Anchor.After);
712645
expect(p4.leftChar()!.view()).toBe('6');
713646
});
647+
648+
test('at end of text should return the last char', () => {
649+
const {peritext} = setupWithChunkedText();
650+
const p1 = peritext.pointAt(8, Anchor.After);
651+
const p2 = peritext.pointAtEnd();
652+
expect(p1.leftChar()!.view()).toBe('9');
653+
expect(p2.leftChar()!.view()).toBe('9');
654+
});
655+
});
656+
657+
describe('.rightChar()', () => {
658+
test('returns the right character', () => {
659+
const model = Model.withLogicalClock(123456);
660+
model.api.root({
661+
text: 'abc',
662+
slices: [],
663+
});
664+
const str = model.api.str(['text']).node;
665+
const peritext = new Peritext(model, str, model.api.arr(['slices']).node);
666+
const point0 = peritext.pointAt(0);
667+
const char0 = point0.rightChar()!;
668+
expect(char0.chunk.data!.slice(char0.off, char0.off + 1)).toBe('a');
669+
const point1 = peritext.pointAt(1);
670+
const char1 = point1.rightChar()!;
671+
expect(char1.chunk.data!.slice(char1.off, char1.off + 1)).toBe('b');
672+
const point2 = peritext.pointAt(2);
673+
const char2 = point2.rightChar()!;
674+
expect(char2.chunk.data!.slice(char2.off, char2.off + 1)).toBe('c');
675+
});
676+
677+
test('multi-char chunks with deletes', () => {
678+
const {peritext} = setupWithText();
679+
const res = '012345678';
680+
for (let i = 0; i < res.length; i++) {
681+
const point = peritext.pointAt(i, Anchor.Before);
682+
const char = point.rightChar()!;
683+
expect(char.view()).toBe(res[i]);
684+
}
685+
for (let i = 0; i < res.length - 1; i++) {
686+
const point = peritext.pointAt(i, Anchor.After);
687+
const char = point.rightChar()!;
688+
expect(char.view()).toBe(res[i + 1]);
689+
}
690+
const end = peritext.pointAt(res.length - 1, Anchor.After);
691+
expect(end.rightChar()).toBe(undefined);
692+
});
693+
694+
test('multi-char chunks with deletes (2)', () => {
695+
const {peritext} = setupWithChunkedText();
696+
const res = '123456789';
697+
for (let i = 0; i < res.length; i++) {
698+
const point = peritext.pointAt(i, Anchor.Before);
699+
const char = point.rightChar()!;
700+
expect(char.view()).toBe(res[i]);
701+
}
702+
for (let i = 0; i < res.length - 1; i++) {
703+
const point = peritext.pointAt(i, Anchor.After);
704+
const char = point.rightChar()!;
705+
expect(char.view()).toBe(res[i + 1]);
706+
}
707+
const end = peritext.pointAt(res.length - 1, Anchor.After);
708+
expect(end.rightChar()).toBe(undefined);
709+
});
710+
711+
test('retrieves right char of a deleted point', () => {
712+
const {peritext, chunkD1, chunkD2} = setupWithChunkedText();
713+
const p1 = peritext.point(chunkD1.id, Anchor.Before);
714+
expect(p1.rightChar()!.view()).toBe('4');
715+
const p2 = peritext.point(chunkD1.id, Anchor.After);
716+
expect(p2.rightChar()!.view()).toBe('4');
717+
const p3 = peritext.point(chunkD2.id, Anchor.Before);
718+
expect(p3.rightChar()!.view()).toBe('7');
719+
const p4 = peritext.point(chunkD2.id, Anchor.After);
720+
expect(p4.rightChar()!.view()).toBe('7');
721+
});
722+
723+
test('at start of text should return the first char', () => {
724+
const {peritext} = setupWithChunkedText();
725+
const p1 = peritext.pointAt(0, Anchor.Before);
726+
const p2 = peritext.pointAtStart();
727+
expect(p1.rightChar()!.view()).toBe('1');
728+
expect(p2.rightChar()!.view()).toBe('1');
729+
});
714730
});
715731

716732
describe('.move()', () => {

0 commit comments

Comments
 (0)