Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export class DraftHandlingService {
return chapterDrafts;
}

async chaptersWithDraft(textDocId: TextDocId, timestamp: Date): Promise<number[]> {
const drafts: Map<string, DeltaOperation[]> = await this.getBookDraft(textDocId, { timestamp });
return Array.from(drafts.keys()).map(chapterNumStr => +chapterNumStr);
}

canApplyDraft(
targetProject: SFProjectProfile,
bookNum: number,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Component, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
import { MatIcon } from '@angular/material/icon';
import { MatProgressBar } from '@angular/material/progress-bar';
Expand Down Expand Up @@ -48,16 +49,46 @@ const mockErrorReportingService = mock(ErrorReportingService);
const mockSFProjectService = mock(SFProjectService);
const mockProjectNotificationService = mock(ProjectNotificationService);

@Component({
standalone: true,
imports: [EditorDraftComponent],
template: `
<app-editor-draft
#editorDraft
[projectId]="projectId"
[bookNum]="bookNum"
[chapter]="chapter"
[isRightToLeft]="isRightToLeft"
[timestamp]="timestamp"
(chaptersUpdated)="onChaptersInDraft($event)"
/>
`
})
class HostEditorDraftComponent {
@ViewChild('editorDraft') editorDraftComponent!: EditorDraftComponent;
projectId = 'targetProjectId';
bookNum = 1;
chapter = 1;
isRightToLeft = false;
timestamp?: Date;
chaptersInDraft: number[] = [];

onChaptersInDraft(chaptersInDraft: number[]): void {
this.chaptersInDraft = chaptersInDraft;
}
}

describe('EditorDraftComponent', () => {
let fixture: ComponentFixture<EditorDraftComponent>;
let fixture: ComponentFixture<HostEditorDraftComponent>;
let component: EditorDraftComponent;
let hostComponent: HostEditorDraftComponent;
let testOnlineStatus: TestOnlineStatusService;
const buildProgress$ = new BehaviorSubject<BuildDto | undefined>(undefined);

configureTestingModule(() => ({
imports: [
HistoryRevisionFormatPipe,
EditorDraftComponent,
HostEditorDraftComponent,
MatProgressBar,
MatSelect,
MatIcon,
Expand Down Expand Up @@ -102,20 +133,27 @@ describe('EditorDraftComponent', () => {
when(mockDraftHandlingService.getBookDraft(anything(), anything())).thenResolve(bookDraftByChapters);
when(mockDraftHandlingService.opsHaveContent(anything())).thenReturn(true);
when(mockSFProjectService.hasDraft(anything(), anything(), anything(), anything())).thenReturn(true);
when(mockSFProjectService.getText(anything())).thenResolve({ isLoaded: false } as any);

fixture = TestBed.createComponent(EditorDraftComponent);
component = fixture.componentInstance;
fixture = TestBed.createComponent(HostEditorDraftComponent);
hostComponent = fixture.componentInstance;

testOnlineStatus = TestBed.inject(OnlineStatusService) as TestOnlineStatusService;
});

function initializeComponent(host: HostEditorDraftComponent): void {
fixture.detectChanges();
component = host.editorDraftComponent;

component.projectId = 'targetProjectId';
component.bookNum = 1;
component.chapter = 1;
component.isRightToLeft = false;
component.ngOnChanges();
});
}

it('should handle offline when component created', fakeAsync(() => {
initializeComponent(hostComponent);
testOnlineStatus.setIsOnline(false);
fixture.detectChanges();
expect(component.draftCheckState).toEqual('draft-unknown');
Expand All @@ -132,6 +170,7 @@ describe('EditorDraftComponent', () => {
of(draftHistory)
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));

fixture.detectChanges();
Expand Down Expand Up @@ -160,6 +199,7 @@ describe('EditorDraftComponent', () => {
when(mockDraftGenerationService.getGeneratedDraftHistory(anything(), anything(), anything())).thenReturn(
of(draftHistory)
);
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));

testOnlineStatus.setIsOnline(false);
Expand Down Expand Up @@ -195,6 +235,7 @@ describe('EditorDraftComponent', () => {
of(draftHistory)
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));

fixture.detectChanges();
Expand All @@ -214,27 +255,32 @@ describe('EditorDraftComponent', () => {
of(draftHistory)
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));

// Initial load fetches draft for current selection.
fixture.detectChanges();
tick(EDITOR_READY_TIMEOUT);
expect(component.bookNum).toBe(1);
expect(hostComponent.chaptersInDraft).toEqual([1, 2]);
verify(mockDraftHandlingService.getBookDraft(anything(), anything())).once();

// Changing chapter triggers another draft retrieval call.
component.chapter = 2;
component.ngOnChanges();
fixture.detectChanges();
tick(EDITOR_READY_TIMEOUT);
expect(hostComponent.chaptersInDraft).toEqual([1, 2]);
verify(mockDraftHandlingService.getBookDraft(anything(), anything())).twice();

// Changing book triggers one more draft retrieval call.
when(mockDraftHandlingService.getBookDraft(anything(), anything())).thenResolve(emptyBookDraftByChapters);
component.bookNum = 2;
component.chapter = 1;
component.ngOnChanges();
fixture.detectChanges();
tick(EDITOR_READY_TIMEOUT);
expect(hostComponent.chaptersInDraft).toEqual([1]);
verify(mockDraftHandlingService.getBookDraft(anything(), anything())).thrice();
flush();
}));
Expand All @@ -247,6 +293,7 @@ describe('EditorDraftComponent', () => {
of(draftHistory)
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));

// Set the date to a time before the earliest draft
Expand All @@ -270,6 +317,7 @@ describe('EditorDraftComponent', () => {
of(draftHistory)
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));

// Set the date to a time just before the earliest draft
Expand All @@ -296,6 +344,7 @@ describe('EditorDraftComponent', () => {
of(draftHistory)
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));

// SUT
Expand All @@ -316,9 +365,10 @@ describe('EditorDraftComponent', () => {
of(draftHistory)
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));
when(mockDraftHandlingService.getBookDraft(anything(), anything())).thenResolve(emptyBookDraftByChapters);
when(mockDraftHandlingService.opsHaveContent(anything())).thenReturn(false);
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));

// SUT
fixture.detectChanges();
Expand All @@ -340,8 +390,10 @@ describe('EditorDraftComponent', () => {
of(draftHistory.slice(0, 1))
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));
when(mockDraftHandlingService.opsHaveContent(anything())).thenReturn(false);
when(mockDraftHandlingService.opsHaveContent(anything())).thenReturn(false);
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));

// SUT
fixture.detectChanges();
Expand All @@ -361,6 +413,7 @@ describe('EditorDraftComponent', () => {
} as SFProjectProfileDoc;
when(mockDraftGenerationService.getGeneratedDraftHistory(anything(), anything(), anything())).thenReturn(of([]));
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));

fixture.detectChanges();
Expand All @@ -379,6 +432,7 @@ describe('EditorDraftComponent', () => {
of(draftHistory)
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));

fixture.detectChanges();
Expand Down Expand Up @@ -419,6 +473,7 @@ describe('EditorDraftComponent', () => {
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
when(mockDialogService.confirm(anything(), anything())).thenResolve(true);
when(mockDraftHandlingService.canApplyDraft(anything(), anything(), anything(), anything())).thenReturn(true);
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops));

fixture.detectChanges();
Expand All @@ -437,6 +492,7 @@ describe('EditorDraftComponent', () => {
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
when(mockDialogService.confirm(anything(), anything())).thenResolve(true);
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops));

fixture.detectChanges();
Expand All @@ -460,6 +516,7 @@ describe('EditorDraftComponent', () => {
of(draftHistory)
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of([]));

fixture.detectChanges();
Expand All @@ -476,6 +533,8 @@ describe('EditorDraftComponent', () => {
}));

it('should throw error if there is no draft', fakeAsync(() => {
when(mockDraftGenerationService.getGeneratedDraftHistory(anything(), anything(), anything())).thenReturn(of([]));
initializeComponent(hostComponent);
component.applyDraft().catch(e => {
expect(e).toEqual(new Error('No draft ops to apply.'));
});
Expand All @@ -491,6 +550,7 @@ describe('EditorDraftComponent', () => {
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
when(mockDialogService.confirm(anything(), anything())).thenResolve(true);
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops));

fixture.detectChanges();
Expand All @@ -516,6 +576,7 @@ describe('EditorDraftComponent', () => {
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
when(mockDialogService.confirm(anything(), anything())).thenResolve(true);
initializeComponent(hostComponent);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops));
fixture.detectChanges();
tick(EDITOR_READY_TIMEOUT);
Expand Down Expand Up @@ -550,10 +611,10 @@ describe('EditorDraftComponent', () => {
when(mockDraftGenerationService.getGeneratedDraftHistory(anything(), anything(), anything())).thenReturn(
of(draftHistory)
);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops));
});

it('should be true when latest build has draft and selected revision is latest', fakeAsync(() => {
initializeComponent(hostComponent);
const testProjectDoc: SFProjectProfileDoc = {
data: createTestProjectProfile({
texts: [
Expand All @@ -579,6 +640,7 @@ describe('EditorDraftComponent', () => {
}));

it('should be false when selected revision is not the latest', fakeAsync(() => {
initializeComponent(hostComponent);
const testProjectDoc: SFProjectProfileDoc = {
data: createTestProjectProfile({
texts: [
Expand Down Expand Up @@ -610,6 +672,7 @@ describe('EditorDraftComponent', () => {
}));

it('should be false when latest build does not have a draft', fakeAsync(() => {
initializeComponent(hostComponent);
const testProjectDoc: SFProjectProfileDoc = {
data: createTestProjectProfile({
texts: [
Expand Down Expand Up @@ -637,6 +700,7 @@ describe('EditorDraftComponent', () => {
}));

it('should be false when latest build is canceled even if draft exists and selected revision is latest', fakeAsync(() => {
initializeComponent(hostComponent);
const testProjectDoc: SFProjectProfileDoc = {
data: createTestProjectProfile({
texts: [
Expand Down Expand Up @@ -665,6 +729,7 @@ describe('EditorDraftComponent', () => {

describe('getLocalizedBookChapter', () => {
it('should return an empty string if bookNum or chapter is undefined', () => {
initializeComponent(hostComponent);
component.bookNum = undefined;
component.chapter = 1;
expect(component['getLocalizedBookChapter']()).toEqual('');
Expand All @@ -675,6 +740,7 @@ describe('EditorDraftComponent', () => {
});

it('should return a localized book and chapter if both are not null', () => {
initializeComponent(hostComponent);
when(mockI18nService.localizeBookChapter(1, 1)).thenReturn('Localized Book 1');
component.bookNum = 1;
component.chapter = 1;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AsyncPipe, NgClass } from '@angular/common';
import { AfterViewInit, Component, DestroyRef, Input, OnChanges, ViewChild } from '@angular/core';
import { AfterViewInit, Component, DestroyRef, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core';
import { MatOption } from '@angular/material/autocomplete';
import { MatButton } from '@angular/material/button';
import { MatFormField } from '@angular/material/form-field';
Expand Down Expand Up @@ -88,6 +88,7 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {
@Input() isRightToLeft!: boolean;
@Input() fontSize?: string;
@Input() timestamp?: Date;
@Output() readonly chaptersUpdated = new EventEmitter<number[]>();

@ViewChild(TextComponent) draftText!: TextComponent;

Expand Down Expand Up @@ -289,7 +290,7 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {
)
)
.subscribe(async ({ targetOps, textDocId, timestamp }) => {
const draftOps: DeltaOperation[] = await this.getChapterDraftOps(textDocId, timestamp);
const draftOps: DeltaOperation[] = await this.getAndRefreshChapters(textDocId, timestamp);
// The user may have navigated to another chapter while this draft was being fetched, in which case the result
// must not be kept as the current chapter's draft. The current chapter's own fetch is either in flight or
// complete, so nothing is lost by discarding this one.
Expand Down Expand Up @@ -447,13 +448,18 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {
);
}

private async getChapterDraftOps(textDocId: TextDocId, timestamp: string): Promise<DeltaOperation[]> {
/**
* Gets the chapters with drafts and emits the chapter numbers.
* Returns the draft for the specified text and timestamp.
*/
private async getAndRefreshChapters(textDocId: TextDocId, timestamp: string): Promise<DeltaOperation[]> {
const chapterNum: string = textDocId.chapterNum.toString();
const timestampAsDate = new Date(timestamp);

const chapterDrafts: Map<string, DeltaOperation[]> = await this.draftHandlingService.getBookDraft(textDocId, {
timestamp: timestampAsDate
});
this.chaptersUpdated.emit(Array.from(chapterDrafts.keys()).map(chapterNumStr => +chapterNumStr));
return chapterDrafts.get(chapterNum) ?? [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
(bookChange)="setBook($event)"
[books]="books"
[(chapter)]="chapter"
[chapters]="chapters"
[chapters]="availableChapters"
></app-book-chapter-chooser>
@if (canShowSourceTab) {
<div class="toolbar-separator">&nbsp;</div>
Expand Down Expand Up @@ -282,6 +282,7 @@
[isRightToLeft]="isTargetRightToLeft"
[fontSize]="fontSize"
[timestamp]="draftTimestamp"
(chaptersUpdated)="onDraftChaptersUpdated($event)"
>
</app-editor-draft>
}
Expand Down
Loading
Loading