diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts index 6ce9b82ba5a..913e73957c5 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts @@ -3373,7 +3373,7 @@ describe('EditorComponent', () => { env.dispose(); })); - it('should remove resolved notes after a remote update', fakeAsync(() => { + it('should remove resolved notes after a remote update', fakeAsync(async () => { const env = new TestEnvironment(); env.setProjectUserConfig(); env.wait(); @@ -3382,7 +3382,7 @@ describe('EditorComponent', () => { let noteThreadEmbedCount = env.countNoteThreadEmbeds(contents.ops!); expect(noteThreadEmbedCount).toEqual(5); - env.resolveNote('project01', 'dataid01'); + await env.resolveNote('project01', 'dataid01'); contents = env.targetEditor.getContents(); noteThreadEmbedCount = env.countNoteThreadEmbeds(contents.ops!); expect(noteThreadEmbedCount).toEqual(4); @@ -5594,10 +5594,9 @@ class TestEnvironment { return noteEmbedCount; } - resolveNote(projectId: string, threadId: string): void { + async resolveNote(projectId: string, threadId: string): Promise { const noteDoc: NoteThreadDoc = this.getNoteThreadDoc(projectId, threadId); - noteDoc.submitJson0Op(op => op.set(n => n.status, NoteStatus.Resolved)); - this.realtimeService.updateQueryAdaptersRemote(); + await this.realtimeService.simulateRemoteChangeJson0Op(noteDoc, op => op.set(n => n.status, NoteStatus.Resolved)); this.wait(); } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/memory-realtime-remote-store.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/memory-realtime-remote-store.ts index c8e20d6b22f..209268aaa9a 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/memory-realtime-remote-store.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/memory-realtime-remote-store.ts @@ -17,6 +17,8 @@ export class MemoryRealtimeRemoteStore extends RealtimeRemoteStore { // getAccessToken is not used in this memory implementation } + /** Write a realtime doc into the store, for a given collection. An existing realtime doc with the same id is + * overwritten. */ addSnapshot(collection: string, snapshot: Snapshot): void { let collectionSnapshots = this.snapshots.get(collection); if (collectionSnapshots == null) { @@ -132,7 +134,7 @@ export class MemoryRealtimeDocAdapter implements RealtimeDocAdapter { return Promise.resolve(); } - submitOp(op: any, source?: any): Promise { + submitOpWithoutEmitting(op: any): void { if (this.type == null) { throw new Error('The doc has not been loaded.'); } @@ -143,6 +145,11 @@ export class MemoryRealtimeDocAdapter implements RealtimeDocAdapter { } this.data = this.type.apply(this.data, op); this.version++; + } + + /** Multiple operations are received for the `op` argument. */ + submitOp(op: any, source?: any): Promise { + this.submitOpWithoutEmitting(op); this.emitChange(op); if (!source) { this.emitRemoteChange(op); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/test-realtime.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/test-realtime.service.ts index ab73583e3fc..e9f9ad5d9da 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/test-realtime.service.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/test-realtime.service.ts @@ -1,9 +1,15 @@ import { Injectable } from '@angular/core'; import { merge } from 'lodash-es'; import * as OTJson0 from 'ot-json0'; +import { Json0OpBuilder } from 'realtime-server/lib/esm/common/utils/json0-op-builder'; import { MemoryOfflineStore } from './memory-offline-store'; -import { MemoryRealtimeQueryAdapter, MemoryRealtimeRemoteStore } from './memory-realtime-remote-store'; +import { + MemoryRealtimeDocAdapter, + MemoryRealtimeQueryAdapter, + MemoryRealtimeRemoteStore +} from './memory-realtime-remote-store'; import { FileOfflineData, FileType } from './models/file-offline-data'; +import { RealtimeDoc } from './models/realtime-doc'; import { Snapshot } from './models/snapshot'; import { RealtimeService } from './realtime.service'; import { objectId } from './utils'; @@ -33,6 +39,8 @@ export class TestRealtimeService extends RealtimeService { } } + /** Write a realtime doc into the store, for a given collection. An existing realtime doc with the same id is + * overwritten. */ addSnapshot(collection: string, snapshot: Partial>, addToOfflineStore: boolean = false): void { const completeSnapshot = addSnapshotDefaults(snapshot); (this.remoteStore as MemoryRealtimeRemoteStore).addSnapshot(collection, completeSnapshot); @@ -57,6 +65,48 @@ export class TestRealtimeService extends RealtimeService { } } + /** Intended to do the same thing as `updateQueryAdaptersRemote` but without remoteChanges$ emitting, which can be + * done in follow up. */ + updateQueryAdaptersRemoteQuietly(): MemoryRealtimeQueryAdapter[] { + const adaptersToEmit: MemoryRealtimeQueryAdapter[] = []; + for (const collectionQueries of this.subscribeQueries.values()) { + for (const query of collectionQueries) { + const adapter = query.adapter as MemoryRealtimeQueryAdapter; + if ((adapter as any).performQuery()) { + adaptersToEmit.push(adapter); + } + } + } + return adaptersToEmit; + } + + /** Simulate a change happening externally. The MemoryRealtimeDocAdapter data and MemoryRealtimeQueryAdapter results + * are updated before changes are announced, so when changes begin to be announced, the docs and queries are all + * up-to-date. The order of emits, and presence or absence of RealtimeQuery.remoteDocChanges$, may be different than + * when running the app. */ + async simulateRemoteChange(doc: RealtimeDoc, ops: any): Promise { + const docAdapter: MemoryRealtimeDocAdapter = doc.adapter as MemoryRealtimeDocAdapter; + // Submitting ops to the realtime doc adapter to simulate writing data on a remote server may seem backwards but + // is getting the job done. + docAdapter.submitOpWithoutEmitting(ops); + const queryAdaptersToEmit: MemoryRealtimeQueryAdapter[] = this.updateQueryAdaptersRemoteQuietly(); + docAdapter.emitChange(ops); + docAdapter.emitRemoteChange(ops); + for (const adapter of queryAdaptersToEmit) { + adapter.remoteChanges$.next(); + } + } + + async simulateRemoteChangeJson0Op( + doc: RealtimeDoc, + build: (builder: Json0OpBuilder) => void + ): Promise { + if (doc.data == null) throw new Error('Cannot simulate remote change on document with null data'); + const builder = new Json0OpBuilder(doc.data) as Json0OpBuilder; + build(builder); + if (builder.op.length > 0) await this.simulateRemoteChange(doc, builder.op); + } + async updateQueriesLocal(): Promise { for (const collectionQueries of this.subscribeQueries.values()) { for (const query of collectionQueries) {