Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,18 @@
</p>
</div>
<div class="question-text">
<p [class]="focusedText" *ngIf="questionAudioUrl">
<p [class]="focusedText" *ngIf="questionAudioUrl || !scriptureAudioUrl">
{{ questionText }}
<mat-icon *ngIf="focusedText === 'scripture-audio-label'" (click)="selectQuestion()">
play_circle_outline
</mat-icon>
</p>
<p [class]="focusedText" *ngIf="!questionAudioUrl" (click)="selectQuestion()" class="clickable">
<p
[class]="focusedText"
*ngIf="!(questionAudioUrl || !scriptureAudioUrl)"
(click)="selectQuestion()"
class="clickable"
>
{{ questionText }}
</p>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Question } from 'realtime-server/lib/esm/scriptureforge/models/question';
import { getTextAudioId, TextAudio } from 'realtime-server/lib/esm/scriptureforge/models/text-audio';
import { TextAudio, getTextAudioId } from 'realtime-server/lib/esm/scriptureforge/models/text-audio';
import { VerseRefData } from 'realtime-server/lib/esm/scriptureforge/models/verse-ref-data';
import { of } from 'rxjs';
import { Subject, of } from 'rxjs';
import { anything, instance, mock, when } from 'ts-mockito';
import { RealtimeQuery } from 'xforge-common/models/realtime-query';
import { PwaService } from 'xforge-common/pwa.service';
import { configureTestingModule, TestTranslocoModule } from 'xforge-common/test-utils';
import { TestTranslocoModule, configureTestingModule } from 'xforge-common/test-utils';
import { UICommonModule } from 'xforge-common/ui-common.module';
import { QuestionDoc } from '../../../../core/models/question-doc';
import { TextAudioDoc } from '../../../../core/models/text-audio-doc';
Expand Down Expand Up @@ -119,8 +119,9 @@ describe('CheckingQuestionComponent', () => {
//play through the scripture audio once
env.scriptureAudio.componentInstance.audio.setSeek(98);
await env.wait();
await env.wait();
env.component.question.playScripture();
await env.wait(1000); //wait for the audio to finish playing
await env.wait(1200); //wait for the audio to finish playing
expect(env.component.question.focusedText).toBe('question-audio-label');

env.component.question.selectScripture();
Expand All @@ -140,7 +141,7 @@ describe('CheckingQuestionComponent', () => {
await env.wait();
await env.wait();

//new question
//new question with matching audio in query
const newQuestionDoc = mock(QuestionDoc);
const newQuestion = mock<Question>();
when(newQuestion.projectRef).thenReturn('project01');
Expand All @@ -154,17 +155,6 @@ describe('CheckingQuestionComponent', () => {
when(newQuestion.verseRef).thenReturn(verseRef);
when(newQuestionDoc.data).thenReturn(instance(newQuestion));

//new scripture audio
const query = mock(RealtimeQuery<TextAudioDoc>) as RealtimeQuery<TextAudioDoc>;
const audioDoc = mock(TextAudioDoc);
const textAudio = mock<TextAudio>();
when(textAudio.audioUrl).thenReturn('test-audio-player.webm');
when(textAudio.timings).thenReturn([]);
when(audioDoc.data).thenReturn(instance(textAudio));
when(audioDoc.id).thenReturn(getTextAudioId('project01', 1, 11));
when(query.docs).thenReturn([instance(audioDoc)]);
when(mockedSFProjectService.queryAudioText('project01')).thenResolve(instance(query));

await env.wait();

expect(env.component.question.questionAudioUrl).toEqual('test-audio-player.webm');
Expand All @@ -177,6 +167,25 @@ describe('CheckingQuestionComponent', () => {
expect(env.component.question.scriptureAudioUrl).toEqual('test-audio-player.webm');
});

it('reloads audio files when audio data changes', async () => {
const env = new TestEnvironment();
await env.wait();
await env.wait();

expect(env.component.question.scriptureAudioUrl).not.toEqual(undefined);
expect(env.component.question.focusedText).toEqual('scripture-audio-label');

//modify the query
when(env.query.docs).thenReturn([]);

//fire the event
env.queryChanged$.next();
await env.wait();

expect(env.component.question.scriptureAudioUrl).toEqual(undefined);
expect(env.component.question.focusedText).toEqual('question-audio-label');
});

it('has default question text', async () => {
const env = new TestEnvironment();
when(mockedQuestion.text).thenReturn('');
Expand All @@ -191,28 +200,38 @@ class TestEnvironment {
readonly component: MockComponent;
readonly fixture: ComponentFixture<MockComponent>;
readonly ngZone: NgZone;
readonly query: RealtimeQuery<TextAudioDoc> = mock(RealtimeQuery<TextAudioDoc>) as RealtimeQuery<TextAudioDoc>;
readonly queryChanged$: Subject<void> = new Subject<void>();

constructor() {
const query = mock(RealtimeQuery<TextAudioDoc>) as RealtimeQuery<TextAudioDoc>;
const audioDoc = mock(TextAudioDoc);
const textAudio = mock<TextAudio>();
when(textAudio.audioUrl).thenReturn('test-audio-player-b.webm');
when(textAudio.timings).thenReturn([]);
when(audioDoc.data).thenReturn(instance(textAudio));
when(audioDoc.id).thenReturn(getTextAudioId('project01', 8, 22));
when(query.docs).thenReturn([instance(audioDoc)]);
const audio1 = this.createTextAudioDoc(getTextAudioId('project01', 8, 22), 'test-audio-player-b.webm');
const audio2 = this.createTextAudioDoc(getTextAudioId('project01', 1, 11), 'test-audio-player.webm');

when(this.query.remoteChanges$).thenReturn(this.queryChanged$);
when(this.query.docs).thenReturn([instance(audio1), instance(audio2)]);

when(mockedPwaService.onlineStatus$).thenReturn(of(true));
when(mockedSFProjectService.onlineIsSourceProject('project01')).thenResolve(false);
when(mockedSFProjectService.onlineDelete(anything())).thenResolve();
when(mockedSFProjectService.onlineUpdateSettings('project01', anything())).thenResolve();
when(mockedSFProjectService.queryAudioText('project01')).thenResolve(instance(query));
when(mockedSFProjectService.queryAudioText('project01')).thenResolve(instance(this.query));

this.ngZone = TestBed.inject(NgZone);
this.fixture = TestBed.createComponent(MockComponent);
this.component = this.fixture.componentInstance;
}

createTextAudioDoc(id: string, url: string): TextAudioDoc {
const audioDoc = mock(TextAudioDoc);
const textAudio = mock<TextAudio>();
when(textAudio.audioUrl).thenReturn(url);
when(textAudio.timings).thenReturn([]);
when(audioDoc.data).thenReturn(instance(textAudio));
when(audioDoc.id).thenReturn(id);

return audioDoc;
}

get scriptureAudio(): DebugElement {
return this.fixture.debugElement.query(By.css('#scriptureAudio'));
}
Expand All @@ -221,7 +240,7 @@ class TestEnvironment {
return this.fixture.debugElement.query(By.css('#questionAudio'));
}

async wait(ms: number = 100): Promise<void> {
async wait(ms: number = 200): Promise<void> {
await new Promise(resolve => this.ngZone.runOutsideAngular(() => setTimeout(resolve, ms)));
this.fixture.detectChanges();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { Component, Input, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';
import { translate } from '@ngneat/transloco';
import { VerseRef } from '@sillsdev/scripture';
import { getTextAudioId, TextAudio } from 'realtime-server/lib/esm/scriptureforge/models/text-audio';
import { TextAudio, getTextAudioId } from 'realtime-server/lib/esm/scriptureforge/models/text-audio';
import {
VerseRefData,
toStartAndEndVerseRefs,
toVerseRef,
VerseRefData
toVerseRef
} from 'realtime-server/lib/esm/scriptureforge/models/verse-ref-data';
import { Subscription } from 'rxjs';
import { TextAudioDoc } from 'src/app/core/models/text-audio-doc';
import { I18nService } from 'xforge-common/i18n.service';
import { RealtimeQuery } from 'xforge-common/models/realtime-query';
import { SubscriptionDisposable } from 'xforge-common/subscription-disposable';
import { QuestionDoc } from '../../../../core/models/question-doc';
import { SFProjectService } from '../../../../core/sf-project.service';
Expand All @@ -18,7 +21,7 @@ import { SingleButtonAudioPlayerComponent } from '../../single-button-audio-play
templateUrl: './checking-question.component.html',
styleUrls: ['./checking-question.component.scss']
})
export class CheckingQuestionComponent extends SubscriptionDisposable implements OnChanges {
export class CheckingQuestionComponent extends SubscriptionDisposable implements OnChanges, OnDestroy {
@Input() questionDoc?: QuestionDoc;
@ViewChild('questionAudio') questionAudio?: SingleButtonAudioPlayerComponent;
@ViewChild('scriptureAudio') set scriptureAudio(comp: SingleButtonAudioPlayerComponent) {
Expand All @@ -36,6 +39,9 @@ export class CheckingQuestionComponent extends SubscriptionDisposable implements
private _scriptureAudio?: SingleButtonAudioPlayerComponent;
private _scriptureTextAudioData?: TextAudio;
private _focusedText: string = 'scripture-audio-label';
private _audioChangeSub?: Subscription;
private audioQuery?: RealtimeQuery<TextAudioDoc>;
private projectId?: string;

constructor(private readonly projectService: SFProjectService, private readonly i18n: I18nService) {
super();
Expand Down Expand Up @@ -72,6 +78,17 @@ export class CheckingQuestionComponent extends SubscriptionDisposable implements
return this.questionDoc?.data?.audioUrl;
}

private get audioId(): string {
if (this.projectId == null) {
return '';
}
return getTextAudioId(
this.projectId,
this.questionDoc!.data!.verseRef!.bookNum,
this.questionDoc!.data!.verseRef!.chapterNum
);
}

private get startVerse(): number {
const verseRefData: VerseRefData | undefined = this.questionDoc?.data?.verseRef;
return verseRefData ? toVerseRef(verseRefData).verseNum : 0;
Expand Down Expand Up @@ -105,18 +122,37 @@ export class CheckingQuestionComponent extends SubscriptionDisposable implements

this._focusedText = 'scripture-audio-label';
const projectId: string = this.questionDoc!.data!.projectRef;
const audioId: string = getTextAudioId(
projectId,
this.questionDoc!.data!.verseRef!.bookNum,
this.questionDoc!.data!.verseRef!.chapterNum
);

this.projectService.queryAudioText(projectId).then(audioQuery => {
this._scriptureTextAudioData = audioQuery?.docs?.find(t => t.id === audioId)?.data;
if (this._scriptureTextAudioData == null || this.scriptureAudioUrl == null) {
this.selectQuestion();
if (projectId === this.projectId) {
this.updateScriptureAudio();
return;
}
this.projectId = projectId;
this.projectService.queryAudioText(this.projectId).then(audioQuery => {
this.audioQuery = audioQuery;
if (this._audioChangeSub != null) {
this._audioChangeSub.unsubscribe();
}
this.updateScriptureAudio();
this._audioChangeSub = audioQuery?.remoteChanges$?.subscribe(() => {
this.updateScriptureAudio();
});
});
}
}

ngOnDestroy(): void {
this.dispose();
if (this._audioChangeSub != null) {
this._audioChangeSub.unsubscribe();
}
}

private updateScriptureAudio(): void {
this._scriptureTextAudioData = this.audioQuery?.docs?.find(t => t.id === this.audioId)?.data;
if (this._scriptureTextAudioData == null || this.scriptureAudioUrl == null) {
this.selectQuestion();
} else {
this.selectScripture();
}
}
}