diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/test-utils.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/test-utils.ts index 946b29f452..03450dd985 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/test-utils.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/test-utils.ts @@ -76,10 +76,12 @@ export function getPoetryVerseTextDoc(id: TextDocId): TextData { return delta; } -export function getEmptyChapterDoc(id: TextDocId): TextData { +export function getEmptyChapterDoc(id: TextDocId, includeHeading: boolean = true): TextData { const delta = new Delta(); - delta.insert({ blank: true }, { segment: 's_1' }); - delta.insert('\n', { para: { style: 's' } }); + if (includeHeading) { + delta.insert({ blank: true }, { segment: 's_1' }); + delta.insert('\n', { para: { style: 's' } }); + } delta.insert({ chapter: { number: id.chapterNum.toString(), style: 'c' } }); delta.insert({ blank: true }, { segment: 's_2' }); delta.insert('\n', { para: { style: 's' } }); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/quill-scripture.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/quill-scripture.ts index 042d5b9e77..5b9c4d7c13 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/quill-scripture.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/quill-scripture.ts @@ -714,6 +714,15 @@ export function registerScripture(): string[] { if (delta == null) { return; } + const sourceDelta: DeltaStatic = delta[source]; + // Workaround to cancel the op if an undo gets triggered that includes inserting a chapter embed. + // The delta diff algorithm has a problem if an op occurs before the first text inserts in a doc. Generally, + // this has no effect, but when edits are made in a blank section heading after a chapter embed at + // index 0, duplicate chapter headings appear and the doc gets corrupted. + if (sourceDelta.ops != null && sourceDelta.ops.filter(o => o.insert?.chapter != null).length > 0) { + this.clear(); + return; + } this.stack[dest].push(delta); this.lastRecorded = 0; this.ignoreChange = true; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/text.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/text.component.spec.ts index 5a0b432aca..913f835f51 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/text.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/text.component.spec.ts @@ -209,6 +209,28 @@ describe('TextComponent', () => { expect(rangePostUndo).toBeTruthy(); })); + it('drops history when edit includes chapter embed', fakeAsync(() => { + const env = new TestEnvironment({ includeHeading: false }); + env.fixture.detectChanges(); + env.id = new TextDocId('project01', 43, 1); + tick(); + env.fixture.detectChanges(); + const updateContentsSpy: jasmine.Spy = spyOn(env.component.editor!, 'updateContents').and.callThrough(); + + const range: RangeStatic = env.component.getSegmentRange('s_1')!; + env.component.editor!.setSelection(range.index + 1, 'user'); + tick(); + env.fixture.detectChanges(); + env.insertText(range.index + 1, 'text'); + expect(env.component.getSegmentText('s_1')).toEqual('text'); + expect(updateContentsSpy.calls.count()).toEqual(2); + + // SUT + env.triggerUndo(); + expect(env.component.getSegmentText('s_1')).toEqual('text'); + expect(updateContentsSpy.calls.count()).toEqual(2); + })); + describe('MultiCursor Presence', () => { it('should not update presence if something other than the user moves the cursor', fakeAsync(() => { const env: TestEnvironment = new TestEnvironment(); @@ -2747,6 +2769,7 @@ interface PerformDropTestArgs { interface TestEnvCtorArgs { chapterNum?: number; textDoc?: RichText.DeltaOperation[]; + includeHeading?: boolean; } class MockDragEvent extends DragEvent { @@ -2817,7 +2840,7 @@ class TestEnvironment { private _onlineStatus = new BehaviorSubject(true); private isOnline: boolean = true; - constructor({ textDoc, chapterNum }: TestEnvCtorArgs = {}) { + constructor({ textDoc, chapterNum, includeHeading }: TestEnvCtorArgs = {}) { when(mockedPwaService.onlineStatus).thenReturn(this._onlineStatus.asObservable()); when(mockedPwaService.isOnline).thenCall(() => this.isOnline); when(mockedTranslocoService.translate(anything())).thenCall( @@ -2848,7 +2871,7 @@ class TestEnvironment { data: getPoetryVerseTextDoc(lukTextDocId), type: RichText.type.name }, - { id: jhnTextDocId.toString(), data: getEmptyChapterDoc(jhnTextDocId), type: RichText.type.name } + { id: jhnTextDocId.toString(), data: getEmptyChapterDoc(jhnTextDocId, includeHeading), type: RichText.type.name } ]); when(mockedProjectService.getText(anything())).thenCall(id =>