From 4fe656054df3709febbcf205302cb01a6c38124f Mon Sep 17 00:00:00 2001 From: Konstantin Schulz Date: Tue, 7 Apr 2020 15:44:18 +0200 Subject: [PATCH] code coverage from unit tests is now at 100% --- .gitlab-ci.yml | 2 +- e2e/src/app.e2e-spec.ts | 4 +- package-lock.json | 2 +- package.json | 6 +- src/app/app-routing.module.ts | 21 +- src/app/app.component.spec.ts | 36 +- src/app/corpus.service.spec.ts | 11 +- src/app/corpus.service.ts | 4 + src/app/exercise-list/exercise-list.page.ts | 6 +- .../exercise-parameters.page.spec.ts | 8 +- .../exercise-parameters.page.ts | 69 ++-- src/app/exercise.service.spec.ts | 8 + src/app/exercise.service.ts | 29 +- src/app/exercise/exercise.page.spec.ts | 9 +- src/app/helper.service.spec.ts | 30 +- src/app/helper.service.ts | 4 + src/app/models/corpusMC.ts | 1 + src/app/models/mockMC.ts | 14 +- src/app/preview/preview.page.spec.ts | 5 +- src/app/preview/preview.page.ts | 37 +- src/app/show-text/show-text.page.spec.ts | 25 +- src/app/test/test.page.html | 29 +- src/app/test/test.page.spec.ts | 98 +++-- src/app/test/test.page.ts | 206 +++++----- src/app/text-range/text-range.page.spec.ts | 387 +++++++++++++++++- src/app/text-range/text-range.page.ts | 355 ++++++++-------- .../translate-testing.module.ts | 7 +- .../vocabulary-check.page.spec.ts | 56 ++- .../vocabulary-check/vocabulary-check.page.ts | 52 +-- src/app/vocabulary.service.spec.ts | 49 ++- src/app/vocabulary.service.ts | 53 +-- src/configMC.ts | 1 + 32 files changed, 1115 insertions(+), 509 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3861031..9854ea1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,7 +4,7 @@ coverage: stage: test script: - docker-compose build - - docker-compose run --rm --entrypoint="npm run test" mc_frontend + - docker-compose run --rm --entrypoint="npm run test-ci" mc_frontend coverage: '/Statements.*?(\d+(?:\.\d+)?)%/' tags: - node diff --git a/e2e/src/app.e2e-spec.ts b/e2e/src/app.e2e-spec.ts index 0362b4b..9f94260 100644 --- a/e2e/src/app.e2e-spec.ts +++ b/e2e/src/app.e2e-spec.ts @@ -1,6 +1,6 @@ import { AppPage } from './app.po'; -describe('new App', () => { +describe('Machina Callida', () => { let page: AppPage; beforeEach(() => { @@ -9,6 +9,6 @@ describe('new App', () => { it('should be blank', () => { page.navigateTo(); - expect(page.getParagraphText()).toContain('The world is your oyster.'); + expect(page.getParagraphText()).toContain('Home'); }); }); diff --git a/package-lock.json b/package-lock.json index a0043e0..8cda9d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mc_frontend", - "version": "1.7.8", + "version": "1.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 2f427b0..a9590c6 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { "name": "mc_frontend", - "version": "1.7.9", + "version": "1.8.0", "author": "Ionic Framework", "homepage": "https://ionicframework.com/", "scripts": { "ng": "ng", "start": "ng serve --port 8100", "build": "ng build", - "test": "ng test --code-coverage --watch=false", - "test-debug": "ng test --watch=true --browsers=Chrome", + "test-ci": "ng test --code-coverage --watch=false", "test-cov": "ng test --code-coverage --watch=true", + "test-debug": "ng test --watch=true --browsers=Chrome", "lint": "ng lint", "e2e": "ng e2e" }, diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 1458846..d2ff25d 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -const routes: Routes = [ +export const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'confirm-cancel', loadChildren: './confirm-cancel/confirm-cancel.module#ConfirmCancelPageModule' }, { path: 'author', loadChildren: './author/author.module#AuthorPageModule' }, @@ -18,15 +18,16 @@ const routes: Routes = [ { path: 'test', loadChildren: './test/test.module#TestPageModule' }, { path: 'text-range', loadChildren: './text-range/text-range.module#TextRangePageModule' }, { path: 'vocabulary-check', loadChildren: './vocabulary-check/vocabulary-check.module#VocabularyCheckPageModule' }, - { path: 'exercise', loadChildren: './exercise/exercise.module#ExercisePageModule' }, { path: 'exercise-list', loadChildren: './exercise-list/exercise-list.module#ExerciseListPageModule' }, - { path: 'doc-voc-unit', loadChildren: './doc-voc-unit/doc-voc-unit.module#DocVocUnitPageModule' }, - { path: 'doc-exercises', loadChildren: './doc-exercises/doc-exercises.module#DocExercisesPageModule' }, - { path: 'doc-software', loadChildren: './doc-software/doc-software.module#DocSoftwarePageModule' }, - { - path: 'semantics', - loadChildren: () => import('./semantics/semantics.module').then( m => m.SemanticsPageModule) - }, - + { path: 'exercise', loadChildren: './exercise/exercise.module#ExercisePageModule' }, + { path: 'exercise-list', loadChildren: './exercise-list/exercise-list.module#ExerciseListPageModule' }, + { path: 'doc-voc-unit', loadChildren: './doc-voc-unit/doc-voc-unit.module#DocVocUnitPageModule' }, + { path: 'doc-exercises', loadChildren: './doc-exercises/doc-exercises.module#DocExercisesPageModule' }, + { path: 'doc-software', loadChildren: './doc-software/doc-software.module#DocSoftwarePageModule' }, + { + path: 'semantics', + loadChildren: () => import('./semantics/semantics.module').then( m => m.SemanticsPageModule) + }, + diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 48459e8..f977d83 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -8,14 +8,22 @@ import {StatusBar} from '@ionic-native/status-bar/ngx'; import {AppComponent} from './app.component'; import {HttpClientModule} from '@angular/common/http'; import {IonicStorageModule} from '@ionic/storage'; -import {AppRoutingModule} from './app-routing.module'; -import {TranslateTestingModule} from './translate-testing/translate-testing.module'; +import {AppRoutingModule, routes} from './app-routing.module'; +import { + FakeLoader, + TranslatePipeMock, + TranslateServiceStub, + TranslateTestingModule +} from './translate-testing/translate-testing.module'; import {APP_BASE_HREF} from '@angular/common'; import {Subscription} from 'rxjs'; import {HelperService} from './helper.service'; import {CorpusService} from './corpus.service'; import Spy = jasmine.Spy; import MockMC from './models/mockMC'; +import {LoadChildrenCallback, Route} from '@angular/router'; +import configMC from '../configMC'; +import {SemanticsPageModule} from './semantics/semantics.module'; describe('AppComponent', () => { let statusBarSpy, splashScreenSpy, platformReadySpy, fixture: ComponentFixture, @@ -92,6 +100,28 @@ describe('AppComponent', () => { expect(languageSpy).toHaveBeenCalledTimes(1); }); - // TODO: add more tests! + it('should test routing', (done) => { + const semanticsRoute: Route = routes.find(x => x.path === configMC.pageUrlSemantics.slice(1)); + const lcb: LoadChildrenCallback = semanticsRoute.loadChildren as LoadChildrenCallback; + const promise: Promise = lcb() as Promise; + promise.then((result: any) => { + expect(result).toBe(SemanticsPageModule); + done(); + }); + }); + it('should test translations', (done) => { + const translateService: TranslateServiceStub = new TranslateServiceStub(); + expect(translateService.getDefaultLang()).toBe('en'); + translateService.get('key').subscribe((result: string) => { + expect(result).toBe('key'); + const translatePipe: TranslatePipeMock = new TranslatePipeMock(); + expect(translatePipe.transform('query')).toBe('query'); + const translateLoader: FakeLoader = new FakeLoader(); + translateLoader.getTranslation('en').subscribe((result2: any) => { + expect(Object.keys(result2).length).toBe(0); + done(); + }); + }); + }); }); diff --git a/src/app/corpus.service.spec.ts b/src/app/corpus.service.spec.ts index 53cacb8..665a02c 100644 --- a/src/app/corpus.service.spec.ts +++ b/src/app/corpus.service.spec.ts @@ -6,7 +6,7 @@ import {RouterModule} from '@angular/router'; import {TranslateTestingModule} from './translate-testing/translate-testing.module'; import {APP_BASE_HREF} from '@angular/common'; import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; -import {HttpClient} from '@angular/common/http'; +import {HttpClient, HttpErrorResponse} from '@angular/common/http'; import {HelperService} from './helper.service'; import MockMC from './models/mockMC'; import {CaseValue, DependencyValue, ExerciseType, PartOfSpeechValue, Phenomenon} from './models/enum'; @@ -118,6 +118,8 @@ describe('CorpusService', () => { corpusService.availableCorpora = []; const requestSpy: Spy = spyOn(helperService, 'makeGetRequest').and.returnValue(Promise.resolve(null)); const localStorageSpy: Spy = spyOn(corpusService, 'loadCorporaFromLocalStorage').and.returnValue(Promise.resolve()); + spyOn(corpusService.storage, 'get').withArgs(configMC.localStorageKeyUpdateInfo).and.returnValue( + Promise.resolve(JSON.stringify(new UpdateInfo()))); corpusService.getCorpora().then(() => { expect(localStorageSpy).toHaveBeenCalledTimes(1); expect(corpusService.availableCorpora.length).toBe(0); @@ -136,10 +138,11 @@ describe('CorpusService', () => { }); it('should get CTS text passage', (done) => { - const spy: Spy = spyOn(helperService, 'makeGetRequest').and.returnValue(Promise.reject(1)); + const spy: Spy = spyOn(helperService, 'makeGetRequest').and.returnValue(Promise.reject( + new HttpErrorResponse({status: 500}))); corpusService.getCTStextPassage('').then(() => { - }, (error: number) => { - expect(error).toBe(1); + }, (error: HttpErrorResponse) => { + expect(error.status).toBe(500); spy.and.returnValue(Promise.resolve( (helperService.deepCopy(MockMC.applicationState) as ApplicationState).mostRecentSetup.annisResponse)); corpusService.getCTStextPassage('').then((ar: AnnisResponse) => { diff --git a/src/app/corpus.service.ts b/src/app/corpus.service.ts index 2c49d5f..0bfcc76 100644 --- a/src/app/corpus.service.ts +++ b/src/app/corpus.service.ts @@ -65,6 +65,8 @@ export class CorpusService { feedback: new Feedback({general: '', incorrect: '', partiallyCorrect: '', correct: ''}), instructionsTranslation: '' }); + public invalidQueryCorpusString: string; + public invalidSentenceCountString: string; public invalidTextRangeString: string; public isTextRangeCorrect = false; public phenomenonMap: PhenomenonMap = new PhenomenonMap({ @@ -291,6 +293,8 @@ export class CorpusService { this.translate.get('DATA_ALREADY_SENT').subscribe(value => this.dataAlreadySentMessage = value); this.translate.get('SEARCH_REGEX_MISSING').subscribe(value => this.searchRegexMissingString = value); this.translate.get('TOO_MANY_SEARCH_RESULTS').subscribe(value => this.tooManyHitsString = value); + this.translate.get('INVALID_SENTENCE_COUNT').subscribe(value => this.invalidSentenceCountString = value); + this.translate.get('INVALID_QUERY_CORPUS').subscribe(value => this.invalidQueryCorpusString = value); } initCorpusService(): Promise { diff --git a/src/app/exercise-list/exercise-list.page.ts b/src/app/exercise-list/exercise-list.page.ts index 712d1e2..45e3579 100644 --- a/src/app/exercise-list/exercise-list.page.ts +++ b/src/app/exercise-list/exercise-list.page.ts @@ -33,7 +33,6 @@ export class ExerciseListPage implements OnInit { public currentSearchValue: string; public currentSortingCategory: SortingCategory = SortingCategory.dateDesc; public exercises: ExerciseMC[] = []; - public ExerciseTypeTranslation = ExerciseTypeTranslation; public hasVocChanged = false; public Math = Math; public metadata: { [eid: string]: string } = {}; @@ -140,7 +139,7 @@ export class ExerciseListPage implements OnInit { } showExercise(exercise: ExerciseMC): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { const url: string = configMC.backendBaseUrl + configMC.backendApiExercisePath; const params: HttpParams = new HttpParams().set('eid', exercise.eid); this.helperService.makeGetRequest(this.http, this.toastCtrl, url, params).then((ar: AnnisResponse) => { @@ -152,9 +151,8 @@ export class ExerciseListPage implements OnInit { this.helperService.goToPreviewPage(this.navCtrl).then(); return resolve(); }, () => { - return reject(); + return resolve(); }); - }).catch(() => { }); } diff --git a/src/app/exercise-parameters/exercise-parameters.page.spec.ts b/src/app/exercise-parameters/exercise-parameters.page.spec.ts index 3d1fdf9..568a8ce 100644 --- a/src/app/exercise-parameters/exercise-parameters.page.spec.ts +++ b/src/app/exercise-parameters/exercise-parameters.page.spec.ts @@ -1,5 +1,5 @@ import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; -import {async, ComponentFixture, fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {ExerciseParametersPage} from './exercise-parameters.page'; import {HttpClientModule} from '@angular/common/http'; @@ -106,19 +106,19 @@ describe('ExerciseParametersPage', () => { }); it('should get exercise data', (done) => { - exerciseParametersPage.corpusService.initCurrentCorpus().then(() => { + exerciseParametersPage.corpusService.initCurrentCorpus().then(async () => { exerciseParametersPage.corpusService.currentTextRange = new ReplaySubject(1); exerciseParametersPage.corpusService.currentTextRange.next(new TextRange({start: ['', ''], end: ['', '']})); const h5pSpy: Spy = spyOn(exerciseParametersPage, 'getH5Pexercise').and.returnValue(Promise.resolve()); const kwicSpy: Spy = spyOn(exerciseParametersPage, 'getKwicExercise').and.returnValue(Promise.resolve()); exerciseParametersPage.corpusService.exercise.type = ExerciseType.kwic; - exerciseParametersPage.getExerciseData(); + await exerciseParametersPage.getExerciseData(); expect(kwicSpy).toHaveBeenCalledTimes(1); exerciseParametersPage.corpusService.exercise.type = ExerciseType.markWords; const pmc: PhenomenonMapContent = exerciseParametersPage.corpusService.phenomenonMap[Phenomenon.partOfSpeech]; pmc.translationValues = {}; pmc.translationValues[PartOfSpeechValue.adjective.toString()] = ''; - exerciseParametersPage.getExerciseData(); + await exerciseParametersPage.getExerciseData(); expect(h5pSpy).toHaveBeenCalledTimes(1); done(); }); diff --git a/src/app/exercise-parameters/exercise-parameters.page.ts b/src/app/exercise-parameters/exercise-parameters.page.ts index 9e5fdd3..79a30de 100644 --- a/src/app/exercise-parameters/exercise-parameters.page.ts +++ b/src/app/exercise-parameters/exercise-parameters.page.ts @@ -60,8 +60,9 @@ export class ExerciseParametersPage implements OnInit { return reject(); } else { this.corpusService.annisResponse.solutions = null; - this.getExerciseData(); - return resolve(); + this.getExerciseData().then(() => { + return resolve(); + }); } }); } @@ -86,36 +87,40 @@ export class ExerciseParametersPage implements OnInit { return translatedKey + ' (' + count + ')'; } - getExerciseData(): void { - const searchValues: string[] = this.corpusService.exercise.queryItems.map( - query => query.phenomenon + '=' + query.values.join('|')); - const formData = new FormData(); - formData.append('urn', this.corpusService.currentUrn); - formData.append('search_values', JSON.stringify(searchValues)); - let instructions: string = this.corpusService.exercise.instructionsTranslation; - if (this.corpusService.exercise.type === ExerciseType.kwic) { - this.getKwicExercise(formData).then(); - return; - } else if (this.corpusService.exercise.type === ExerciseType.markWords) { - const phenomenon: Phenomenon = this.corpusService.exercise.queryItems[0].phenomenon; - const pmc: PhenomenonMapContent = this.corpusService.phenomenonMap[phenomenon]; - const values: string[] = this.corpusService.exercise.queryItems[0].values as string[]; - instructions += ` [${values.map(x => pmc.translationValues[x]).join(', ')}]`; - } - this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { - this.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { - // TODO: change the corpus title to something meaningful, e.g. concatenate user ID and wanted exercise title - const workTitle: string = cc.title + ', ' + tr.start.filter(x => x).join('.') + '-' + tr.end.filter(x => x).join('.'); - formData.append('work_title', workTitle); - formData.append('type', MoodleExerciseType[this.corpusService.exercise.type]); - formData.append('type_translation', this.corpusService.exercise.typeTranslation); - formData.append('instructions', instructions); - formData.append('correct_feedback', this.corpusService.exercise.feedback.correct); - formData.append('partially_correct_feedback', this.corpusService.exercise.feedback.partiallyCorrect); - formData.append('incorrect_feedback', this.corpusService.exercise.feedback.incorrect); - formData.append('general_feedback', this.corpusService.exercise.feedback.general); - formData.append('work_author', cc.author); - this.getH5Pexercise(formData).then(); + getExerciseData(): Promise { + return new Promise(resolve => { + const searchValues: string[] = this.corpusService.exercise.queryItems.map( + query => query.phenomenon + '=' + query.values.join('|')); + const formData = new FormData(); + formData.append('urn', this.corpusService.currentUrn); + formData.append('search_values', JSON.stringify(searchValues)); + let instructions: string = this.corpusService.exercise.instructionsTranslation; + if (this.corpusService.exercise.type === ExerciseType.kwic) { + this.getKwicExercise(formData).then(); + return resolve(); + } else if (this.corpusService.exercise.type === ExerciseType.markWords) { + const phenomenon: Phenomenon = this.corpusService.exercise.queryItems[0].phenomenon; + const pmc: PhenomenonMapContent = this.corpusService.phenomenonMap[phenomenon]; + const values: string[] = this.corpusService.exercise.queryItems[0].values as string[]; + instructions += ` [${values.map(x => pmc.translationValues[x]).join(', ')}]`; + } + this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { + this.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { + // TODO: change the corpus title to something meaningful, e.g. concatenate user ID and wanted exercise title + const workTitle: string = cc.title + ', ' + tr.start.filter(x => x).join('.') + '-' + tr.end.filter(x => x).join('.'); + formData.append('work_title', workTitle); + formData.append('type', MoodleExerciseType[this.corpusService.exercise.type]); + formData.append('type_translation', this.corpusService.exercise.typeTranslation); + formData.append('instructions', instructions); + formData.append('correct_feedback', this.corpusService.exercise.feedback.correct); + formData.append('partially_correct_feedback', this.corpusService.exercise.feedback.partiallyCorrect); + formData.append('incorrect_feedback', this.corpusService.exercise.feedback.incorrect); + formData.append('general_feedback', this.corpusService.exercise.feedback.general); + formData.append('work_author', cc.author); + this.getH5Pexercise(formData).then(() => { + return resolve(); + }); + }); }); }); } diff --git a/src/app/exercise.service.spec.ts b/src/app/exercise.service.spec.ts index 96a4896..3a7fc7f 100644 --- a/src/app/exercise.service.spec.ts +++ b/src/app/exercise.service.spec.ts @@ -3,11 +3,19 @@ import {TestBed} from '@angular/core/testing'; import {ExerciseService} from './exercise.service'; import {APP_BASE_HREF} from '@angular/common'; import configMC from '../configMC'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {IonicStorageModule} from '@ionic/storage'; +import {TranslateTestingModule} from './translate-testing/translate-testing.module'; describe('ExerciseService', () => { let exerciseService: ExerciseService; beforeEach(() => { TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + IonicStorageModule.forRoot(), + TranslateTestingModule, + ], providers: [ {provide: APP_BASE_HREF, useValue: '/'}, ], diff --git a/src/app/exercise.service.ts b/src/app/exercise.service.ts index 1e9369b..abcd1ff 100644 --- a/src/app/exercise.service.ts +++ b/src/app/exercise.service.ts @@ -1,5 +1,8 @@ /* tslint:disable:no-string-literal */ import {Injectable} from '@angular/core'; +import configMC from '../configMC'; +import {HelperService} from './helper.service'; +import {ExercisePart} from './models/exercisePart'; declare var H5P: any; // dirty hack to prevent H5P access errors after resize events @@ -15,6 +18,25 @@ window.onresize = () => { providedIn: 'root' }) export class ExerciseService { + // tslint:disable-next-line:variable-name + private _currentExerciseIndex: number; + get currentExerciseIndex(): number { + return this._currentExerciseIndex; + } + + set currentExerciseIndex(value: number) { + this._currentExerciseIndex = value; + this.currentExercisePartIndex = [...Array(this.currentExerciseParts.length).keys()].find( + i => this.currentExerciseParts[i].startIndex <= this.currentExerciseIndex && (!this.currentExerciseParts[i + 1] + || this.currentExerciseParts[i + 1].startIndex > this.currentExerciseIndex)); + const cepi: number = this.currentExercisePartIndex; + this.currentExerciseName = this.currentExercisePartIndex ? + this.currentExerciseParts[cepi].exercises[this.currentExerciseIndex - this.currentExerciseParts[cepi].startIndex] : ''; + } + + public currentExerciseName: string; + public currentExercisePartIndex: number; + public currentExerciseParts: ExercisePart[]; public excludeOOV = false; public fillBlanksString = 'fill_blanks'; public h5pContainerString = '.h5p-container'; @@ -22,7 +44,7 @@ export class ExerciseService { public kwicGraphs: string; public vocListString = 'voc_list'; - constructor() { + constructor(public helperService: HelperService) { } createGuid(): string { @@ -50,4 +72,9 @@ export class ExerciseService { }, 50); }); } + + setH5Purl(url: string): void { + // this has to be LocalStorage because the H5P javascript cannot easily access the Ionic Storage + window.localStorage.setItem(configMC.localStorageKeyH5P, url); + } } diff --git a/src/app/exercise/exercise.page.spec.ts b/src/app/exercise/exercise.page.spec.ts index 9bae06e..0133b0b 100644 --- a/src/app/exercise/exercise.page.spec.ts +++ b/src/app/exercise/exercise.page.spec.ts @@ -11,6 +11,7 @@ import {AnnisResponse} from '../models/annisResponse'; import {ExerciseType, MoodleExerciseType} from '../models/enum'; import Spy = jasmine.Spy; import configMC from '../../configMC'; +import MockMC from '../models/mockMC'; describe('ExercisePage', () => { let exercisePage: ExercisePage; @@ -50,15 +51,19 @@ describe('ExercisePage', () => { }); it('should be initialized', (done) => { - const loadExerciseSpy: Spy = spyOn(exercisePage, 'loadExercise'); + const loadExerciseSpy: Spy = spyOn(exercisePage, 'loadExercise').and.returnValue(Promise.resolve()); checkSpy.and.callFake(() => Promise.reject()); exercisePage.ngOnInit().then(() => { expect(loadExerciseSpy).toHaveBeenCalledTimes(0); - done(); + checkSpy.and.returnValue(Promise.resolve()); + exercisePage.ngOnInit().then(() => { + done(); + }); }); }); it('should load the exercise', (done) => { + exercisePage.helperService.applicationState.next(exercisePage.helperService.deepCopy(MockMC.applicationState)); exercisePage.loadExercise().then(() => { expect(exercisePage.corpusService.exercise.type).toBe(ExerciseType.cloze); getRequestSpy.and.returnValue(Promise.resolve(new AnnisResponse({exercise_type: MoodleExerciseType.markWords.toString()}))); diff --git a/src/app/helper.service.spec.ts b/src/app/helper.service.spec.ts index dfd162b..4fc17a5 100644 --- a/src/app/helper.service.spec.ts +++ b/src/app/helper.service.spec.ts @@ -114,25 +114,23 @@ describe('HelperService', () => { const navFnArr: any[] = [helperService.goToAuthorDetailPage, helperService.goToDocExercisesPage, helperService.goToDocSoftwarePage, helperService.goToDocVocUnitPage, helperService.goToExerciseListPage, helperService.goToExerciseParametersPage, helperService.goToImprintPage, helperService.goToInfoPage, helperService.goToPreviewPage, helperService.goToSourcesPage, - helperService.goToTextRangePage, helperService.goToVocabularyCheckPage, helperService.goToKwicPage]; + helperService.goToTextRangePage, helperService.goToVocabularyCheckPage, helperService.goToKwicPage, + helperService.goToRankingPage, helperService.goToSemanticsPage]; const pageUrlArr: string[] = [configMC.pageUrlAuthorDetail, configMC.pageUrlDocExercises, configMC.pageUrlDocSoftware, configMC.pageUrlDocVocUnit, configMC.pageUrlExerciseList, configMC.pageUrlExerciseParameters, configMC.pageUrlImprint, configMC.pageUrlInfo, configMC.pageUrlPreview, configMC.pageUrlSources, configMC.pageUrlTextRange, - configMC.pageUrlVocabularyCheck, configMC.pageUrlKwic]; - checkNavigation(navFnArr, pageUrlArr, navCtrl, forwardSpy).then(() => { - helperService.goToAuthorPage(navCtrl).then(() => { - expect(helperService.isVocabularyCheck).toBeFalsy(); - helperService.goToShowTextPage(navCtrl, true).then(() => { - expect(helperService.isVocabularyCheck).toBe(true); - const rootSpy: Spy = spyOn(navCtrl, 'navigateRoot').and.returnValue(Promise.resolve(true)); - helperService.goToHomePage(navCtrl).then(() => { - expect(rootSpy).toHaveBeenCalledWith(configMC.pageUrlHome); - helperService.goToTestPage(navCtrl).then(() => { - expect(rootSpy).toHaveBeenCalledWith(configMC.pageUrlTest); - done(); - }); - }); - }); + configMC.pageUrlVocabularyCheck, configMC.pageUrlKwic, configMC.pageUrlRanking, configMC.pageUrlSemantics]; + checkNavigation(navFnArr, pageUrlArr, navCtrl, forwardSpy).then(async () => { + await helperService.goToAuthorPage(navCtrl); + expect(helperService.isVocabularyCheck).toBeFalsy(); + await helperService.goToShowTextPage(navCtrl, true); + expect(helperService.isVocabularyCheck).toBe(true); + const rootSpy: Spy = spyOn(navCtrl, 'navigateRoot').and.returnValue(Promise.resolve(true)); + await helperService.goToHomePage(navCtrl); + expect(rootSpy).toHaveBeenCalledWith(configMC.pageUrlHome); + helperService.goToTestPage(navCtrl).then(() => { + expect(rootSpy).toHaveBeenCalledWith(configMC.pageUrlTest); + done(); }); }); }); diff --git a/src/app/helper.service.ts b/src/app/helper.service.ts index 43c4e57..f1d3ebd 100644 --- a/src/app/helper.service.ts +++ b/src/app/helper.service.ts @@ -233,6 +233,10 @@ export class HelperService { return navCtrl.navigateForward(configMC.pageUrlPreview); } + goToRankingPage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlRanking); + } + goToSemanticsPage(navCtrl: NavController): Promise { return navCtrl.navigateForward(configMC.pageUrlSemantics); } diff --git a/src/app/models/corpusMC.ts b/src/app/models/corpusMC.ts index 142402c..0762ae5 100644 --- a/src/app/models/corpusMC.ts +++ b/src/app/models/corpusMC.ts @@ -1,3 +1,4 @@ +/* tslint:disable:variable-name */ import {Citation} from 'src/app/models/citation'; export class CorpusMC { diff --git a/src/app/models/mockMC.ts b/src/app/models/mockMC.ts index b659621..d6f66ad 100644 --- a/src/app/models/mockMC.ts +++ b/src/app/models/mockMC.ts @@ -10,6 +10,8 @@ import {TestResultMC} from './testResultMC'; import StatementBase from './xAPI/StatementBase'; import Result from './xAPI/Result'; import Score from './xAPI/Score'; +import {TextRange} from './textRange'; +import {Citation} from './citation'; export default class MockMC { static apiResponseCorporaGet: object = { @@ -19,13 +21,6 @@ export default class MockMC { title: 'title', })] }; - static apiResponseExerciseListGet: ExerciseMC[] = [new ExerciseMC({ - eid: 'eid', - exercise_type: MoodleExerciseType.cloze.toString(), - exercise_type_translation: 'exercise_type_translation', - work_author: 'work_author', - work_title: 'work_title', - })]; static apiResponseFrequencyAnalysisGet: FrequencyItem[] = [new FrequencyItem({ phenomena: [Phenomenon.partOfSpeech.toString()], values: [PartOfSpeechValue.adjective.toString()] @@ -35,7 +30,10 @@ export default class MockMC { links: [] }); static applicationState: ApplicationState = new ApplicationState({ - currentSetup: new TextData({currentCorpus: new CorpusMC()}), + currentSetup: new TextData({ + currentCorpus: new CorpusMC({citations: {}}), + currentTextRange: new TextRange({start: ['1', '2'], end: ['1', '2']}) + }), mostRecentSetup: new TextData({annisResponse: new AnnisResponse({nodes: [new NodeMC()], links: []})}), exerciseList: [new ExerciseMC()] }); diff --git a/src/app/preview/preview.page.spec.ts b/src/app/preview/preview.page.spec.ts index 97c2707..1bf652a 100644 --- a/src/app/preview/preview.page.spec.ts +++ b/src/app/preview/preview.page.spec.ts @@ -29,6 +29,7 @@ describe('PreviewPage', () => { let fixture: ComponentFixture; let corpusService: CorpusService; let checkAnnisResponseSpy: Spy; + let xapiSpy: Spy; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -50,6 +51,7 @@ describe('PreviewPage', () => { corpusService = TestBed.inject(CorpusService); fixture = TestBed.createComponent(PreviewPage); previewPage = fixture.componentInstance; + xapiSpy = spyOn(previewPage, 'setXAPIeventHandler'); checkAnnisResponseSpy = spyOn(corpusService, 'checkAnnisResponse').and.callFake(() => Promise.reject()); fixture.detectChanges(); })); @@ -93,9 +95,10 @@ describe('PreviewPage', () => { body.appendChild(iframe); spyOn(previewPage, 'sendData').and.returnValue(Promise.resolve()); previewPage.ngOnDestroy(); - const oldDispatcher: any = previewPage.helperService.deepCopy(H5P.externalDispatcher); const newDispatcher: H5PeventDispatcherMock = new H5PeventDispatcherMock(); + const oldDispatcher: any = previewPage.helperService.deepCopy(H5P.externalDispatcher); H5P.externalDispatcher = newDispatcher; + xapiSpy.and.callThrough(); previewPage.ngOnInit().then(() => { newDispatcher.triggerXAPI(configMC.xAPIverbIDanswered, new Result()); checkAnnisResponseSpy.and.returnValue(Promise.resolve()); diff --git a/src/app/preview/preview.page.ts b/src/app/preview/preview.page.ts index 940bc43..9bd1010 100644 --- a/src/app/preview/preview.page.ts +++ b/src/app/preview/preview.page.ts @@ -58,8 +58,7 @@ export class PreviewPage implements OnDestroy, OnInit { // this will be called via GET request from the h5p standalone javascript library const url: string = `${configMC.backendBaseUrl + configMC.backendApiH5pPath}` + `?eid=${this.corpusService.annisResponse.exercise_id}&lang=${this.translateService.currentLang + solutionIndicesString}`; - // this has to be LocalStorage because the H5P javascript cannot easily access the Ionic Storage - window.localStorage.setItem(configMC.localStorageKeyH5P, url); + this.exerciseService.setH5Purl(url); const exerciseTypePath: string = this.corpusService.exercise.type === ExerciseType.markWords ? 'mark_words' : 'drag_text'; this.exerciseService.initH5P(exerciseTypePath).then(); this.updateFileUrl(); @@ -75,21 +74,7 @@ export class PreviewPage implements OnDestroy, OnInit { if (!this.helperService.isVocabularyCheck) { this.exerciseService.excludeOOV = false; } - H5P.externalDispatcher.on('xAPI', (event: XAPIevent) => { - // results are only available when a task has been completed/answered, not in the "attempted" or "interacted" stages - if (event.data.statement.verb.id === configMC.xAPIverbIDanswered && event.data.statement.result) { - const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); - if (iframe) { - const iframeDoc: Document = iframe.contentWindow.document; - const inner: string = iframeDoc.documentElement.innerHTML; - const result: TestResultMC = new TestResultMC({ - statement: event.data.statement, - innerHTML: inner - }); - this.sendData(result).then(); - } - } - }); + this.setXAPIeventHandler(); this.corpusService.checkAnnisResponse().then(() => { this.processAnnisResponse(this.corpusService.annisResponse); this.initH5P(); @@ -149,6 +134,24 @@ export class PreviewPage implements OnDestroy, OnInit { }); } + setXAPIeventHandler() { + H5P.externalDispatcher.on('xAPI', (event: XAPIevent) => { + // results are only available when a task has been completed/answered, not in the "attempted" or "interacted" stages + if (event.data.statement.verb.id === configMC.xAPIverbIDanswered && event.data.statement.result) { + const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); + if (iframe) { + const iframeDoc: Document = iframe.contentWindow.document; + const inner: string = iframeDoc.documentElement.innerHTML; + const result: TestResultMC = new TestResultMC({ + statement: event.data.statement, + innerHTML: inner + }); + this.sendData(result).then(); + } + } + }); + } + switchOOV(): void { this.currentSolutions = []; this.processSolutions(this.corpusService.annisResponse.solutions); diff --git a/src/app/show-text/show-text.page.spec.ts b/src/app/show-text/show-text.page.spec.ts index 590cbcf..87ccf6a 100644 --- a/src/app/show-text/show-text.page.spec.ts +++ b/src/app/show-text/show-text.page.spec.ts @@ -12,6 +12,7 @@ import {AnnisResponse} from '../models/annisResponse'; import {NodeMC} from '../models/nodeMC'; import {VocabularyCorpus} from '../models/enum'; import Spy = jasmine.Spy; +import MockMC from '../models/mockMC'; describe('ShowTextPage', () => { let showTextPage: ShowTextPage; @@ -46,22 +47,22 @@ describe('ShowTextPage', () => { }); it('should generate a download link', (done) => { + showTextPage.corpusService.initCurrentCorpus(); + showTextPage.helperService.applicationState.next(showTextPage.helperService.deepCopy(MockMC.applicationState)); showTextPage.corpusService.currentText = 'text'; fixture.detectChanges(); const requestSpy: Spy = spyOn(showTextPage.helperService, 'makePostRequest').and.returnValue(Promise.resolve('a.b.c')); - showTextPage.corpusService.initCurrentCorpus().then(() => { + showTextPage.generateDownloadLink('').then(() => { + let link: HTMLLinkElement = document.querySelector(showTextPage.downloadLinkSelector); + expect(link.href.length).toBe(61); + requestSpy.and.callFake(() => Promise.reject()); + link.style.display = 'none'; + fixture.detectChanges(); showTextPage.generateDownloadLink('').then(() => { - let link: HTMLLinkElement = document.querySelector(showTextPage.downloadLinkSelector); - expect(link.href.length).toBe(61); - requestSpy.and.callFake(() => Promise.reject()); - link.style.display = 'none'; - fixture.detectChanges(); - showTextPage.generateDownloadLink('').then(() => { - }, () => { - link = document.querySelector(showTextPage.downloadLinkSelector); - expect(link.style.display).toBe('none'); - done(); - }); + }, () => { + link = document.querySelector(showTextPage.downloadLinkSelector); + expect(link.style.display).toBe('none'); + done(); }); }); }); diff --git a/src/app/test/test.page.html b/src/app/test/test.page.html index 6306358..e2af583 100644 --- a/src/app/test/test.page.html +++ b/src/app/test/test.page.html @@ -1,7 +1,7 @@
- {{ (currentExerciseIndex == 0 ? 'TEST' : (isTestMode ? 'START_TEST' : 'START_LEARNING')) | translate }} + {{ (exerciseService.currentExerciseIndex == 0 ? 'TEST' : (isTestMode ? 'START_TEST' : 'START_LEARNING')) | translate }}
@@ -19,16 +19,16 @@ Show exercise: - + {{ number + 1 }} Show solutions for: - + {{ number + 1 }} @@ -38,8 +38,8 @@ {{ 'TEST_MODULE_GO_TO_EXERCISE' | translate}}: - + {{ number + 1 }} @@ -48,17 +48,17 @@ - {{ 'TEST_MODULE_PROGRESS_PART' | translate }} {{getCurrentExercisePartIndex() + 1}} + {{ 'TEST_MODULE_PROGRESS_PART' | translate }} {{exerciseService.currentExercisePartIndex + 1}} - + - {{ 'TEST_MODULE_EXERCISE_ID' | translate }}: #{{ currentExerciseIndex + 1 }} + {{ 'TEST_MODULE_EXERCISE_ID' | translate }}: #{{ exerciseService.currentExerciseIndex + 1 }} -
+

{{ 'UNIT_INTRO_TITLE' | translate }}

@@ -101,13 +101,14 @@
+ *ngIf="(!isTestMode || currentState == TestModuleState.showSolutions) && + !exerciseService.currentExerciseName.startsWith(nonH5Pstring)"> {{ 'BUTTON_CONTINUE' | translate}} -
+

{{'UNIT_EVALUATION_HEADER' | translate}}

@@ -163,8 +164,6 @@
- - diff --git a/src/app/test/test.page.spec.ts b/src/app/test/test.page.spec.ts index 7a1d5e6..92a494e 100644 --- a/src/app/test/test.page.spec.ts +++ b/src/app/test/test.page.spec.ts @@ -1,5 +1,5 @@ import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; import {TestPage} from './test.page'; import {IonicStorageModule} from '@ionic/storage'; @@ -7,7 +7,6 @@ import {RouterModule} from '@angular/router'; import {TranslateTestingModule} from '../translate-testing/translate-testing.module'; import {PopoverController} from '@ionic/angular'; import {APP_BASE_HREF} from '@angular/common'; -import {HttpClientTestingModule} from '@angular/common/http/testing'; import {TestResultMC} from '../models/testResultMC'; import StatementBase from '../models/xAPI/StatementBase'; import Result from '../models/xAPI/Result'; @@ -23,6 +22,8 @@ import Context from '../models/xAPI/Context'; import ContextActivities from '../models/xAPI/ContextActivities'; import Activity from '../models/xAPI/Activity'; import Definition from '../models/xAPI/Definition'; +import {HttpClientModule} from '@angular/common/http'; +import {By} from '@angular/platform-browser'; declare var H5P: any; @@ -34,7 +35,7 @@ describe('TestPage', () => { TestBed.configureTestingModule({ declarations: [TestPage], imports: [ - HttpClientTestingModule, + HttpClientModule, IonicStorageModule.forRoot(), RouterModule.forRoot([]), TranslateTestingModule, @@ -59,12 +60,13 @@ describe('TestPage', () => { it('should adjust the timer', () => { testPage.isTestMode = false; - const timerElement: HTMLSpanElement = document.querySelector(testPage.timerIDstring); + const timerElement: HTMLSpanElement = fixture.debugElement.query(By.css(testPage.timerIDstring)).nativeElement; timerElement.innerHTML = '1'; testPage.adjustTimer(1, false); expect(document.querySelector(testPage.timerIDstring).innerHTML).toBe('1'); testPage.isTestMode = true; - testPage.adjustTimer(testPage.currentExerciseParts[testPage.currentExerciseParts.length - 1].startIndex, true); + testPage.adjustTimer(testPage.exerciseService.currentExerciseParts[testPage.exerciseService.currentExerciseParts.length - 1] + .startIndex, true); expect(document.querySelector(testPage.timerIDstring).innerHTML).toBe('1'); testPage.adjustTimer(1, false); expect(document.querySelector(testPage.timerIDstring).innerHTML).toBe(testPage.timerValueZero); @@ -86,10 +88,10 @@ describe('TestPage', () => { }); it('should continue to the next exercise', () => { - spyOn(testPage, 'showNextExercise'); - testPage.currentExerciseIndex = 0; + spyOn(testPage, 'showNextExercise').and.returnValue(Promise.resolve()); + testPage.exerciseService.currentExerciseIndex = 0; testPage.continueToNextExercise(false); - expect(testPage.currentExerciseIndex).toBe(1); + expect(testPage.exerciseService.currentExerciseIndex).toBe(1); }); it('should attempt to exit', (done) => { @@ -115,8 +117,8 @@ describe('TestPage', () => { }); it('should get the current exercise name', () => { - testPage.currentExerciseIndex = 10; - const name: string = testPage.getCurrentExerciseName(); + testPage.exerciseService.currentExerciseIndex = 10; + const name: string = testPage.exerciseService.currentExerciseName; expect(name.length).toBe(15); }); @@ -138,9 +140,9 @@ describe('TestPage', () => { }); it('should reset the test environment', () => { - testPage.currentExerciseIndex = 1; + testPage.exerciseService.currentExerciseIndex = 1; testPage.resetTestEnvironment(); - expect(testPage.currentExerciseIndex).toBe(0); + expect(testPage.exerciseService.currentExerciseIndex).toBe(0); }); it('should save the current result', () => { @@ -148,7 +150,7 @@ describe('TestPage', () => { const input: HTMLInputElement = iframe.contentWindow.document.createElement('input'); input.setAttribute('id', testPage.h5pKnownIDstring.slice(1)); iframe.contentWindow.document.body.appendChild(input); - testPage.currentExerciseIndex = 5; + testPage.exerciseService.currentExerciseIndex = 5; testPage.knownCount = [0, 0]; testPage.saveCurrentExerciseResult(true, new XAPIevent({data: {statement: new StatementBase()}})); expect(testPage.knownCount[0]).toBe(0); @@ -193,7 +195,7 @@ describe('TestPage', () => { testPage.currentState = TestModuleState.inProgress; newDispatcher.trigger('xAPI', xapiEvent); expect(finishSpy).toHaveBeenCalledTimes(1); - const inputEventSpy: Spy = spyOn(testPage, 'triggerInputEventHandler'); + const inputEventSpy: Spy = spyOn(testPage, 'setInputEventHandler'); const solutionsEventSpy: Spy = spyOn(testPage, 'triggerSolutionsEventHandler'); testPage.currentState = TestModuleState.inProgress; const domChangedEvent: any = {data: {library: testPage.h5pBlanksString}}; @@ -206,27 +208,48 @@ describe('TestPage', () => { expect(solutionsEventSpy).toHaveBeenCalledTimes(1); }); - it('should show the next exercise', () => { - const h5pSpy: Spy = spyOn(testPage.exerciseService, 'initH5P').and.returnValue(Promise.resolve()); - const exerciseNameSpy: Spy = spyOn(testPage, 'getCurrentExerciseName').and.returnValue(testPage.exerciseService.vocListString); + it('should show the next exercise', (done) => { + spyOn(testPage, 'triggerSolutionsEventHandler'); const resultsSpy: Spy = spyOn(testPage, 'analyzeResults'); const hideButtonSpy: Spy = spyOn(testPage, 'hideRetryButton'); - const iframe: HTMLIFrameElement = MockMC.addIframe(testPage.exerciseService.h5pIframeString); - testPage.showNextExercise(1); - expect(h5pSpy).toHaveBeenCalledTimes(1); - testPage.vocService.currentTestResults[2] = new TestResultMC({ - statement: new StatementBase({ - context: - new Context({contextActivities: new ContextActivities({category: [new Activity({id: testPage.h5pDragTextString})]})}) - }) + let targetExercisePartIndex: number = testPage.exerciseService.currentExerciseParts + .findIndex(x => x.exercises.some(y => y.startsWith(testPage.exerciseService.vocListString))); + if (targetExercisePartIndex < 0) { + testPage.exerciseService.currentExerciseParts.push(testPage.availableExerciseParts[4]); + testPage.adjustStartIndices(); + targetExercisePartIndex = testPage.exerciseService.currentExerciseParts.length - 1; + } + const previousExercises: number[] = testPage.exerciseService.currentExerciseParts.slice(0, targetExercisePartIndex) + .map(x => x.exercises.length); + testPage.exerciseService.currentExerciseIndex = previousExercises.reduce((a, b) => a + b); + let wasH5Prendered = false; + H5P.externalDispatcher.on('domChanged', async (event: any) => { + wasH5Prendered = !wasH5Prendered; + if (wasH5Prendered) { + const url: string = window.localStorage.getItem(configMC.localStorageKeyH5P); + const result: any = await testPage.http.get(url).toPromise(); + const iframe2: HTMLIFrameElement = document.querySelector(testPage.exerciseService.h5pIframeString); + const parEl: HTMLParagraphElement = iframe2.contentWindow.document.querySelector('p'); + expect(result.text).toContain(parEl.textContent); + H5P.externalDispatcher.off('domChanged'); + testPage.vocService.currentTestResults[2] = new TestResultMC({ + statement: new StatementBase({ + context: new Context({ + contextActivities: new ContextActivities({category: [new Activity({id: testPage.h5pDragTextString})]}) + }) + }) + }); + testPage.exerciseService.currentExerciseIndex = 2; + await testPage.showNextExercise(2, true); + expect(hideButtonSpy).toHaveBeenCalledTimes(1); + testPage.exerciseService.currentExerciseName = testPage.nonH5Pstring; + await testPage.showNextExercise(testPage.exerciseService.currentExerciseParts + [testPage.exerciseService.currentExerciseParts.length - 1].startIndex); + expect(resultsSpy).toHaveBeenCalledTimes(1); + done(); + } }); - testPage.currentExerciseIndex = 2; - testPage.showNextExercise(2, true); - expect(hideButtonSpy).toHaveBeenCalledTimes(1); - exerciseNameSpy.and.returnValue(testPage.nonH5Pstring); - testPage.showNextExercise(testPage.currentExerciseParts[testPage.currentExerciseParts.length - 1].startIndex); - expect(resultsSpy).toHaveBeenCalledTimes(1); - iframe.parentNode.removeChild(iframe); + testPage.showNextExercise(testPage.exerciseService.currentExerciseIndex).then(); }); it('should trigger the input event handler', () => { @@ -236,7 +259,7 @@ describe('TestPage', () => { const input: HTMLInputElement = iframe.contentWindow.document.createElement('input'); input.classList.add(testPage.h5pTextInputClassString.slice(1)); iframe.contentWindow.document.body.appendChild(input); - testPage.triggerInputEventHandler(); + testPage.setInputEventHandler(); const inputs: NodeListOf = iframe.contentWindow.document.querySelectorAll(testPage.h5pTextInputClassString); const kbe: KeyboardEvent = new KeyboardEvent('keydown', {key: 'Enter'}); inputs[0].dispatchEvent(kbe); @@ -247,7 +270,7 @@ describe('TestPage', () => { it('should trigger the solutions event handler', () => { const hideButtonSpy: Spy = spyOn(testPage, 'hideRetryButton'); const iframe: HTMLIFrameElement = MockMC.addIframe(testPage.exerciseService.h5pIframeString, testPage.h5pCheckButtonClassString); - testPage.currentExerciseIndex = 0; + testPage.exerciseService.currentExerciseIndex = 0; const description = 'description'; testPage.vocService.currentTestResults[0] = new TestResultMC({ statement: new StatementBase({ @@ -279,12 +302,13 @@ describe('TestPage', () => { iframe.parentNode.removeChild(iframe); }); - it('should update the timer', () => { + it('should update the timer', fakeAsync(() => { testPage.countDownDateTime = new Date().getTime() - 1; const iframe: HTMLIFrameElement = MockMC.addIframe(testPage.exerciseService.h5pIframeString, testPage.h5pCheckButtonClassString); - const showNextSpy: Spy = spyOn(testPage, 'showNextExercise'); + const showNextSpy: Spy = spyOn(testPage, 'showNextExercise').and.returnValue(Promise.resolve()); testPage.updateTimer(); expect(showNextSpy).toHaveBeenCalledTimes(1); iframe.parentNode.removeChild(iframe); - }); + tick(testPage.finishExerciseTimeout); + })); }); diff --git a/src/app/test/test.page.ts b/src/app/test/test.page.ts index 907f346..ac41bee 100644 --- a/src/app/test/test.page.ts +++ b/src/app/test/test.page.ts @@ -1,5 +1,5 @@ /* tslint:disable:no-string-literal */ -import {Component, OnDestroy, OnInit} from '@angular/core'; +import {Component, NgZone, OnDestroy, OnInit} from '@angular/core'; import {NavController, PopoverController, ToastController} from '@ionic/angular'; import {HelperService} from '../helper.service'; import {XAPIevent} from 'src/app/models/xAPIevent'; @@ -77,8 +77,6 @@ export class TestPage implements OnDestroy, OnInit { }), new ExercisePart({exercises: ['nonH5P_2'], startIndex: 0})]; public configMC = configMC; public countDownDateTime: number; - public currentExerciseIndex: number; - public currentExerciseParts: ExercisePart[]; public currentState: TestModuleState = TestModuleState.inProgress; public didTimeRunOut = false; public exerciseIndices: number[]; @@ -115,13 +113,15 @@ export class TestPage implements OnDestroy, OnInit { public exerciseService: ExerciseService, public storage: Storage, public helperService: HelperService, - public corpusService: CorpusService) { + public corpusService: CorpusService, + public ngZone: NgZone) { } addScore(allTestIndices: number[], exercisePartIndex: number): void { const relevantTestIndices = allTestIndices.filter( - x => this.currentExerciseParts[exercisePartIndex].startIndex <= x && (!this - .currentExerciseParts[exercisePartIndex + 1] || x < this.currentExerciseParts[exercisePartIndex + 1].startIndex)); + x => this.exerciseService.currentExerciseParts[exercisePartIndex].startIndex <= x && + (!this.exerciseService.currentExerciseParts[exercisePartIndex + 1] || + x < this.exerciseService.currentExerciseParts[exercisePartIndex + 1].startIndex)); const correctlySolved = relevantTestIndices.filter((i) => { return this.vocService.currentTestResults[i].statement.result.score.scaled === 1; }); @@ -129,13 +129,13 @@ export class TestPage implements OnDestroy, OnInit { } adjustStartIndices(): void { - this.currentExerciseParts[0].startIndex = 0; - [...Array(this.currentExerciseParts.length).keys()].forEach((index: number) => { + this.exerciseService.currentExerciseParts[0].startIndex = 0; + [...Array(this.exerciseService.currentExerciseParts.length).keys()].forEach((index: number) => { if (index === 0) { return; } - this.currentExerciseParts[index].startIndex = this.currentExerciseParts[index - 1] - .startIndex + this.currentExerciseParts[index - 1].exercises.length; + this.exerciseService.currentExerciseParts[index].startIndex = this.exerciseService.currentExerciseParts[index - 1] + .startIndex + this.exerciseService.currentExerciseParts[index - 1].exercises.length; }); } @@ -144,36 +144,39 @@ export class TestPage implements OnDestroy, OnInit { return; } if (!review) { - const metaIndex: number = this.currentExerciseParts.map(x => x.startIndex).indexOf(newIndex); - if (metaIndex > -1 && this.currentExerciseParts[metaIndex].durationSeconds > 0) { + const metaIndex: number = this.exerciseService.currentExerciseParts.map(x => x.startIndex).indexOf(newIndex); + if (metaIndex > -1 && this.exerciseService.currentExerciseParts[metaIndex].durationSeconds > 0) { this.removeTimer(false); - this.initTimer(this.currentExerciseParts[metaIndex].durationSeconds); + this.initTimer(this.exerciseService.currentExerciseParts[metaIndex].durationSeconds); } } - if (newIndex === this.currentExerciseParts[this.currentExerciseParts.length - 1].startIndex) { + if (newIndex === this.exerciseService.currentExerciseParts[this.exerciseService.currentExerciseParts.length - 1].startIndex) { this.removeTimer(true); } } + analyzePretestResults(allTestIndices: number[], relevantTestIndices: number[], correctlySolved: number[]): void { + relevantTestIndices = allTestIndices.filter(x => x < this.exerciseService.currentExerciseParts[this.resultsBaseIndex].startIndex); + correctlySolved = relevantTestIndices.filter((i) => { + return this.vocService.currentTestResults[i].statement.result.score.scaled === 1; + }); + this.results.push([correctlySolved.length, relevantTestIndices.length]); + } + analyzeResults(): void { this.results = []; this.resultsBaseIndex = this.isTestMode ? 2 : 1; - const allTestIndices = Object.keys(this.vocService.currentTestResults).map(x => +x); + const allTestIndices: number[] = Object.keys(this.vocService.currentTestResults).map(x => +x); let relevantTestIndices: number[]; let correctlySolved: number[]; if (this.isTestMode) { - // pretest - relevantTestIndices = allTestIndices.filter(x => x < this.currentExerciseParts[this.resultsBaseIndex].startIndex); - correctlySolved = relevantTestIndices.filter((i) => { - return this.vocService.currentTestResults[i].statement.result.score.scaled === 1; - }); - this.results.push([correctlySolved.length, relevantTestIndices.length]); + this.analyzePretestResults(allTestIndices, relevantTestIndices, correctlySolved); } // text comprehension this.addScore(allTestIndices, this.resultsBaseIndex); // exercises - relevantTestIndices = allTestIndices.filter(x => this.currentExerciseParts[this.resultsBaseIndex + 1].startIndex - <= x && x < this.currentExerciseParts[this.resultsBaseIndex + 2].startIndex); + relevantTestIndices = allTestIndices.filter(x => this.exerciseService.currentExerciseParts[this.resultsBaseIndex + 1].startIndex + <= x && x < this.exerciseService.currentExerciseParts[this.resultsBaseIndex + 2].startIndex); correctlySolved = relevantTestIndices.map(i => this.vocService.currentTestResults[i].statement.result.score.raw); const scoreObserved: number = correctlySolved.length ? correctlySolved.reduce((x, y) => x + y) : 0; const scoreExpected: number = relevantTestIndices.length ? relevantTestIndices.map( @@ -197,22 +200,27 @@ export class TestPage implements OnDestroy, OnInit { } continueToNextExercise(isTestMode: boolean = true): void { - if (this.isTestMode && !isTestMode) { - // no pretest in learning mode - this.deleteExercisePart(1); - } - this.isTestMode = isTestMode; - this.currentExerciseIndex = this.currentExerciseIndex + 1; - this.showNextExercise(this.currentExerciseIndex, this.currentState === TestModuleState.showSolutions); + // this needs to run in the angular zone to update data bindings in the view immediately + this.ngZone.run(() => { + if (this.isTestMode && !isTestMode) { + // no pretest in learning mode + this.deleteExercisePart(1); + } + this.isTestMode = isTestMode; + this.exerciseService.currentExerciseIndex = this.exerciseService.currentExerciseIndex + 1; + this.showNextExercise(this.exerciseService.currentExerciseIndex, + this.currentState === TestModuleState.showSolutions).then(); + }); } deleteExercisePart(index: number): void { this.exerciseIndices = []; - this.currentExerciseParts.splice(index, 1); + this.exerciseService.currentExerciseParts.splice(index, 1); this.adjustStartIndices(); // dirty hack to make the view re-render any ngFor references to the exerciseIndices setTimeout(() => { - const exerciseCount: number = this.currentExerciseParts.map(x => x.exercises.length).reduce((x, y) => x + y); + const exerciseCount: number = this.exerciseService.currentExerciseParts.map(x => x.exercises.length) + .reduce((x, y) => x + y); this.exerciseIndices = Array.from(new Array(exerciseCount).keys()); }, 50); } @@ -236,21 +244,6 @@ export class TestPage implements OnDestroy, OnInit { }); } - getCurrentExerciseName(): string { - const targetPartIndex: number = this.getCurrentExercisePartIndex(); - if (!targetPartIndex) { - return ''; - } - return this.currentExerciseParts[targetPartIndex] - .exercises[this.currentExerciseIndex - this.currentExerciseParts[targetPartIndex].startIndex]; - } - - getCurrentExercisePartIndex(): number { - return [...Array(this.currentExerciseParts.length).keys()].find( - i => this.currentExerciseParts[i].startIndex <= this.currentExerciseIndex && (!this.currentExerciseParts[i + 1] - || this.currentExerciseParts[i + 1].startIndex > this.currentExerciseIndex)); - } - hideRetryButton(): void { const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); const iframeDoc: Document = iframe.contentWindow.document; @@ -265,7 +258,7 @@ export class TestPage implements OnDestroy, OnInit { // add the new duration to countdown this.countDownDateTime = new Date(new Date().getTime() + durationSeconds * 1000).getTime(); // Update the countdown every 1 second - this.timer = setInterval(this.updateTimer, 1000); + this.timer = setInterval(this.updateTimer.bind(this), 1000); } ngOnDestroy(): void { @@ -279,7 +272,7 @@ export class TestPage implements OnDestroy, OnInit { this.randomizeTestType(); this.results = []; this.wasDataSent = false; - this.currentExerciseIndex = 0; + this.exerciseService.currentExerciseIndex = 0; this.vocService.currentTestResults = {}; this.currentState = TestModuleState.inProgress; this.knownCount = [0, 0]; @@ -291,11 +284,11 @@ export class TestPage implements OnDestroy, OnInit { } randomizeTestType(): void { - this.currentExerciseParts = this.availableExerciseParts.slice(); + this.exerciseService.currentExerciseParts = this.availableExerciseParts.slice(); // remove either the second last or third last exercise const index: number = Math.random() < 0.5 ? 3 : 4; this.testType = index === 3 ? TestType.cloze : TestType.list; - this.deleteExercisePart(this.currentExerciseParts.length - index); + this.deleteExercisePart(this.exerciseService.currentExerciseParts.length - index); } removeTimer(freeze: boolean): void { @@ -316,7 +309,7 @@ export class TestPage implements OnDestroy, OnInit { if (iframe) { const iframeDoc: Document = iframe.contentWindow.document; const inner: string = iframeDoc.documentElement.innerHTML; - this.vocService.currentTestResults[this.currentExerciseIndex] = new TestResultMC({ + this.vocService.currentTestResults[this.exerciseService.currentExerciseIndex] = new TestResultMC({ statement: event.data.statement, innerHTML: inner }); @@ -370,7 +363,7 @@ export class TestPage implements OnDestroy, OnInit { // dirty hack because domChanged events are triggered twice for every new H5P exercise if (!this.areEventHandlersSet) { if (this.currentState === TestModuleState.inProgress && event.data.library === this.h5pBlanksString) { - this.triggerInputEventHandler(); + this.setInputEventHandler(); } else if (this.currentState === TestModuleState.showSolutions) { this.triggerSolutionsEventHandler(); } @@ -379,46 +372,7 @@ export class TestPage implements OnDestroy, OnInit { }); } - showNextExercise(newIndex: number, review: boolean = false): void { - this.adjustTimer(newIndex, review); - const currentExercisePart: ExercisePart = this.currentExerciseParts[this.getCurrentExercisePartIndex()]; - const maxProgress: number = currentExercisePart.exercises.length; - this.progressBarValue = (newIndex - currentExercisePart.startIndex) / maxProgress; - this.updateUI(); - const currentExerciseName: string = this.getCurrentExerciseName(); - if (currentExerciseName.startsWith(this.nonH5Pstring)) { - document.querySelector(this.h5pRowIDstring).classList.add(this.hideClassString); - if (newIndex === this.currentExerciseParts[this.currentExerciseParts.length - 1].startIndex) { - this.analyzeResults(); - H5P.externalDispatcher.off('xAPI'); - this.updateUI(); - } - return; - } - this.currentState = review ? TestModuleState.showSolutions : TestModuleState.inProgress; - document.querySelector(this.h5pRowIDstring).classList.remove(this.hideClassString); - if (review && this.vocService.currentTestResults[this.currentExerciseIndex]) { - const id = this.vocService.currentTestResults[this.currentExerciseIndex].statement.context.contextActivities.category[0].id; - // handle the drag text exercise solutions - if (id.indexOf(this.h5pDragTextString) > -1) { - const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); - const iframeDoc: Document = iframe.contentWindow.document; - iframeDoc.documentElement.innerHTML = this.vocService.currentTestResults[this.currentExerciseIndex].innerHTML; - this.hideRetryButton(); - return; - } - } - const fileName: string = currentExerciseName.split('_').slice(-1) + '_' + this.translate.currentLang + '.json'; - let exerciseType = currentExerciseName.split('_').slice(0, 2).join('_'); - this.storage.set(configMC.localStorageKeyH5P, this.helperService.baseUrl + '/assets/h5p/' - + exerciseType + '/content/' + fileName).then(); - if (exerciseType.startsWith(this.exerciseService.vocListString)) { - exerciseType = this.exerciseService.fillBlanksString; - } - this.exerciseService.initH5P(exerciseType).then(); - } - - triggerInputEventHandler(): void { + setInputEventHandler(): void { const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); if (iframe) { const inputs: NodeList = iframe.contentWindow.document.querySelectorAll(this.h5pTextInputClassString); @@ -439,13 +393,59 @@ export class TestPage implements OnDestroy, OnInit { } } + showNextExercise(newIndex: number, review: boolean = false): Promise { + return new Promise(resolve => { + this.adjustTimer(newIndex, review); + const currentExercisePart: ExercisePart = this.exerciseService.currentExerciseParts + [this.exerciseService.currentExercisePartIndex]; + const maxProgress: number = currentExercisePart.exercises.length; + this.progressBarValue = (newIndex - currentExercisePart.startIndex) / maxProgress; + if (this.exerciseService.currentExerciseName.startsWith(this.nonH5Pstring)) { + document.querySelector(this.h5pRowIDstring).classList.add(this.hideClassString); + if (newIndex === + this.exerciseService.currentExerciseParts[this.exerciseService.currentExerciseParts.length - 1].startIndex) { + this.analyzeResults(); + H5P.externalDispatcher.off('xAPI'); + } + return resolve(); + } + this.currentState = review ? TestModuleState.showSolutions : TestModuleState.inProgress; + document.querySelector(this.h5pRowIDstring).classList.remove(this.hideClassString); + if (review && this.vocService.currentTestResults[this.exerciseService.currentExerciseIndex]) { + const id = this.vocService.currentTestResults[this.exerciseService.currentExerciseIndex] + .statement.context.contextActivities.category[0].id; + // handle the drag text exercise solutions + if (id.indexOf(this.h5pDragTextString) > -1) { + const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); + const iframeDoc: Document = iframe.contentWindow.document; + iframeDoc.documentElement.innerHTML = this.vocService.currentTestResults[this.exerciseService.currentExerciseIndex] + .innerHTML; + this.hideRetryButton(); + return resolve(); + } + } + const fileName: string = this.exerciseService.currentExerciseName.split('_').slice(-1) + '_' + + this.translate.currentLang + '.json'; + let exerciseType = this.exerciseService.currentExerciseName.split('_').slice(0, 2).join('_'); + this.exerciseService.setH5Purl(this.helperService.baseUrl + '/assets/h5p/' + exerciseType + '/content/' + fileName); + if (exerciseType.startsWith(this.exerciseService.vocListString)) { + exerciseType = this.exerciseService.fillBlanksString; + } + this.exerciseService.initH5P(exerciseType).then(() => { + return resolve(); + }); + }); + } + triggerSolutionsEventHandler(): void { const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); if (iframe) { - if (this.vocService.currentTestResults[this.currentExerciseIndex]) { - const oldActivity: Activity = this.vocService.currentTestResults[this.currentExerciseIndex].statement.object as Activity; - const oldContext: Context = this.vocService.currentTestResults[this.currentExerciseIndex].statement.context; - const oldResponse: string = this.vocService.currentTestResults[this.currentExerciseIndex].statement.result.response; + if (this.vocService.currentTestResults[this.exerciseService.currentExerciseIndex]) { + const oldActivity: Activity = this.vocService.currentTestResults[this.exerciseService.currentExerciseIndex] + .statement.object as Activity; + const oldContext: Context = this.vocService.currentTestResults[this.exerciseService.currentExerciseIndex].statement.context; + const oldResponse: string = this.vocService.currentTestResults[this.exerciseService.currentExerciseIndex] + .statement.result.response; const singleResponses: string[] = oldResponse.split('[,]'); if (oldContext.contextActivities.category[0].id.indexOf(this.h5pMultiChoiceString) > -1) { const oldChosen: { description: LanguageMap, id: string }[] = oldActivity.definition.choices.filter( @@ -505,14 +505,10 @@ export class TestPage implements OnDestroy, OnInit { }, this.finishExerciseTimeout); } } - const newIndex: number = this.currentExerciseParts[this.getCurrentExercisePartIndex() + 1].startIndex; - this.currentExerciseIndex = newIndex; - this.showNextExercise(newIndex); + const newIndex: number = this.exerciseService.currentExerciseParts[this.exerciseService.currentExercisePartIndex + 1] + .startIndex; + this.exerciseService.currentExerciseIndex = newIndex; + this.showNextExercise(newIndex).then(); } } - - updateUI(): void { - // dirty hack to trigger ngIf evaluation & data bindings - (document.querySelector('#refreshUI') as HTMLLinkElement).click(); - } } diff --git a/src/app/text-range/text-range.page.spec.ts b/src/app/text-range/text-range.page.spec.ts index 59c1759..67892a4 100644 --- a/src/app/text-range/text-range.page.spec.ts +++ b/src/app/text-range/text-range.page.spec.ts @@ -8,9 +8,14 @@ import {RouterModule} from '@angular/router'; import {TranslateTestingModule} from '../translate-testing/translate-testing.module'; import {FormsModule} from '@angular/forms'; import {APP_BASE_HREF} from '@angular/common'; -import {CorpusService} from '../corpus.service'; -import {ReplaySubject} from 'rxjs'; import {CorpusMC} from '../models/corpusMC'; +import {Citation} from '../models/citation'; +import Spy = jasmine.Spy; +import MockMC from '../models/mockMC'; +import {take} from 'rxjs/operators'; +import {CitationLevel} from '../models/enum'; +import {TextRange} from '../models/textRange'; +import {ReplaySubject} from 'rxjs'; describe('TextRangePage', () => { let textRangePage: TextRangePage; @@ -28,20 +33,392 @@ describe('TextRangePage', () => { ], providers: [ {provide: APP_BASE_HREF, useValue: '/'}, - {provide: CorpusService, useValue: {currentCorpus: new ReplaySubject(1)}} ], schemas: [CUSTOM_ELEMENTS_SCHEMA], - }) - .compileComponents().then(); + }).compileComponents().then(); })); beforeEach(() => { fixture = TestBed.createComponent(TextRangePage); textRangePage = fixture.componentInstance; + textRangePage.corpusService.currentCorpus = new ReplaySubject(1); fixture.detectChanges(); }); it('should create', () => { expect(textRangePage).toBeTruthy(); }); + + it('should add level 3 references', (done) => { + const addReferencesSpy: Spy = spyOn(textRangePage, 'addReferences').and.returnValue(Promise.resolve()); + textRangePage.addLevel3References([], new CorpusMC()).then(() => { + expect(addReferencesSpy).toHaveBeenCalledTimes(0); + textRangePage.currentInputId = 2; + const corpus: CorpusMC = new CorpusMC({ + citations: {1: new Citation({subcitations: {2: new Citation({subcitations: {3: new Citation()}})}})} + }); + textRangePage.addLevel3References(['1', '2'], corpus).then(() => { + expect(addReferencesSpy).toHaveBeenCalledTimes(0); + corpus.citations['1'].subcitations['2'].subcitations = {}; + textRangePage.addLevel3References(['1', '2'], corpus).then(() => { + expect(addReferencesSpy).toHaveBeenCalledTimes(1); + done(); + }); + }); + }); + }); + + it('should add missing citations', (done) => { + function expectCitationLength(expectedLength: number) { + expect(textRangePage.citationValuesStart.length).toBe(expectedLength); + expect(textRangePage.citationValuesEnd.length).toBe(expectedLength); + } + + function resetCitationValues(): void { + textRangePage.citationValuesStart = []; + textRangePage.citationValuesEnd = []; + } + + textRangePage.corpusService.currentCorpus = new ReplaySubject(1); + textRangePage.corpusService.currentCorpus.next(new CorpusMC({citations: {4: new Citation()}})); + resetCitationValues(); + const citationLabels: string[] = ['1']; + textRangePage.addMissingCitations(citationLabels, citationLabels).then(() => { + expectCitationLength(1); + citationLabels.push('b'); + resetCitationValues(); + textRangePage.addMissingCitations(citationLabels, citationLabels).then(() => { + }, () => { + expectCitationLength(1); + citationLabels[1] = '2'; + resetCitationValues(); + textRangePage.addMissingCitations(citationLabels, citationLabels).then(() => { + expectCitationLength(2); + citationLabels.push('3'); + resetCitationValues(); + textRangePage.addMissingCitations(citationLabels, citationLabels).then(() => { + expectCitationLength(3); + done(); + }); + }); + }); + }); + }); + + it('should add references', (done) => { + const corpus: CorpusMC = new CorpusMC({citations: {0: new Citation({subcitations: {}, label: ''})}}); + textRangePage.corpusService.currentCorpus = new ReplaySubject(1); + textRangePage.corpusService.currentCorpus.next(corpus); + const validReffSpy: Spy = spyOn(textRangePage.corpusService, 'getCTSvalidReff').and.callFake(() => Promise.reject()); + textRangePage.addReferences('', [null]).then(() => { + expect(Object.keys(corpus.citations).length).toBe(1); + textRangePage.addReferences('', [new Citation()]).then(() => { + }, async () => { + expect(Object.keys(corpus.citations).length).toBe(1); + validReffSpy.and.returnValue(Promise.resolve(['1'])); + const citations: Citation[] = [new Citation({isNumeric: true, value: 0, label: '0'})]; + await textRangePage.addReferences('', citations); + expect(corpus.citations['0'].subcitations['1'].label).toBe('1'); + validReffSpy.and.returnValue(Promise.resolve(['a'])); + await textRangePage.addReferences('', []); + expect(corpus.citations.a.label).toBe('a'); + citations.push(new Citation({label: '1'})); + await textRangePage.addReferences('', citations); + expect(textRangePage.currentlyAvailableCitations.slice(-1)[0]).toBe('.1.a'); + done(); + }); + }); + }); + + it('should apply autocomplete', (done) => { + spyOn(textRangePage, 'showFurtherReferences').and.returnValue(Promise.resolve()); + textRangePage.currentInputId = 1; + const input: HTMLInputElement = document.createElement('input'); + input.setAttribute('id', 'input2'); + document.body.appendChild(input); + textRangePage.applyAutoComplete(true).then(() => { + expect(document.activeElement).toBe(input); + input.parentNode.removeChild(input); + done(); + }); + }); + + it('should check whether input is disabled', (done) => { + textRangePage.corpusService.initCurrentTextRange(); + textRangePage.helperService.applicationState.next(textRangePage.helperService.deepCopy(MockMC.applicationState)); + textRangePage.corpusService.currentCorpus = new ReplaySubject(1); + textRangePage.corpusService.currentCorpus.next(new CorpusMC({citations: {4: new Citation()}})); + textRangePage.checkInputDisabled().then(() => { + textRangePage.isInputDisabled[0].pipe(take(1)).subscribe((isDisabled: boolean) => { + expect(isDisabled).toBe(true); + textRangePage.corpusService.currentCorpus.next(new CorpusMC({ + citations: {1: new Citation({subcitations: {2: new Citation()}})} + })); + textRangePage.checkInputDisabled().then(() => { + textRangePage.isInputDisabled[0].pipe(take(1)).subscribe((isDisabled2: boolean) => { + expect(isDisabled2).toBe(false); + done(); + }); + }); + }); + }); + }); + + it('should check the text range', (done) => { + function expectRangeCheckResult(start: string[], end: string[], expectedResult: boolean): Promise { + return new Promise(resolve => textRangePage.checkTextRange(start, end).then((result: boolean) => { + expect(result).toBe(expectedResult); + return resolve(); + })); + } + + const corpus: CorpusMC = new CorpusMC({ + citations: {4: new Citation()}, + citation_level_2: CitationLevel.default.toString(), + citation_level_3: CitationLevel.default.toString(), + }); + textRangePage.corpusService.currentCorpus = new ReplaySubject(1); + textRangePage.corpusService.currentCorpus.next(corpus); + const citationLabels: string[] = ['1', '2', '3']; + expectRangeCheckResult(citationLabels.slice(0, 1), [], false).then(() => { + corpus.citation_level_2 = ''; + expectRangeCheckResult(citationLabels.slice(0, 2), [], false).then(() => { + expectRangeCheckResult(citationLabels.slice(0, 2), citationLabels, false).then(() => { + corpus.citation_level_3 = ''; + expectRangeCheckResult(citationLabels, citationLabels.slice(0, 2), false).then(() => { + const addCitSpy: Spy = spyOn(textRangePage, 'addMissingCitations').and.returnValue(Promise.resolve()); + spyOn(textRangePage, 'compareCitationValues').and.returnValue(Promise.resolve(true)); + expectRangeCheckResult(citationLabels, citationLabels, true).then(() => { + addCitSpy.and.callFake(() => Promise.reject()); + expectRangeCheckResult(citationLabels, citationLabels, true).then(() => { + done(); + }); + }); + }); + }); + }); + }); + }); + + it('should compare citation values', (done) => { + function expectComparisonResult(start: number[], end: number[], expectedResult: boolean): Promise { + textRangePage.citationValuesStart = start; + textRangePage.citationValuesEnd = end; + return new Promise(resolve => textRangePage.compareCitationValues().then((result: boolean) => { + expect(result).toBe(expectedResult); + return resolve(); + })); + } + + expectComparisonResult([0], [1], true).then(() => { + expectComparisonResult([NaN], [1], true).then(() => { + expectComparisonResult([2], [1], false).then(() => { + expectComparisonResult([1], [1], true).then(() => { + expectComparisonResult([1, 0], [1, 1], true).then(() => { + expectComparisonResult([1, 2], [1, 1], false).then(() => { + expectComparisonResult([1, 1], [1, 1], true).then(() => { + expectComparisonResult([1, 1, 1], [1, 1, 0], false).then(() => { + done(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + + it('should confirm the selection', (done) => { + function expectNavigationCalled(spy: Spy, times: number = 1, skipText: boolean = false): Promise { + return new Promise(resolve => { + textRangePage.confirmSelection(skipText).then(() => { + expect(spy).toHaveBeenCalledTimes(times); + return resolve(); + }); + }); + } + + textRangePage.isTextRangeCheckRunning = true; + textRangePage.citationValuesStart = [NaN]; + textRangePage.citationValuesEnd = []; + const checkSpy: Spy = spyOn(textRangePage, 'checkTextRange').and.returnValue(Promise.resolve(false)); + expectNavigationCalled(checkSpy, 0).then(async () => { + textRangePage.isTextRangeCheckRunning = false; + textRangePage.corpusService.currentCorpus = new ReplaySubject(1); + textRangePage.corpusService.currentCorpus.next(new CorpusMC({ + citations: {4: new Citation()}, + citation_level_2: CitationLevel.default.toString() + })); + textRangePage.corpusService.initCurrentTextRange(); + textRangePage.helperService.applicationState.next(textRangePage.helperService.deepCopy(MockMC.applicationState)); + const getTextSpy: Spy = spyOn(textRangePage.corpusService, 'getText').and.callFake(() => Promise.reject()); + await expectNavigationCalled(getTextSpy, 0); + checkSpy.and.returnValue(Promise.resolve(true)); + const showTextSpy: Spy = spyOn(textRangePage.helperService, 'goToShowTextPage').and.returnValue(Promise.resolve(true)); + await expectNavigationCalled(showTextSpy, 0); + getTextSpy.and.returnValue(Promise.resolve()); + await expectNavigationCalled(showTextSpy); + textRangePage.helperService.isVocabularyCheck = true; + const vocCheckSpy: Spy = spyOn(textRangePage.helperService, 'goToVocabularyCheckPage') + .and.returnValue(Promise.resolve(true)); + await expectNavigationCalled(vocCheckSpy); + textRangePage.citationValuesStart = []; + const exParamSpy: Spy = spyOn(textRangePage.helperService, 'goToExerciseParametersPage') + .and.returnValue(Promise.resolve(true)); + await expectNavigationCalled(exParamSpy, 1, true); + done(); + }); + }); + + it('should initialize the page', (done) => { + textRangePage.corpusService.initCurrentTextRange(); + textRangePage.helperService.applicationState.next(textRangePage.helperService.deepCopy(MockMC.applicationState)); + textRangePage.initPage(new CorpusMC({ + citation_level_2: CitationLevel.default.toString(), + citations: {1: new Citation({label: '1'})} + })).then(() => { + textRangePage.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { + expect(tr.start[0]).toBe('1'); + done(); + }); + }); + }); + + it('should map citation labels', (done) => { + function expectValueList(label: string, index: number, citLabels: string[], values: number[], + expectedValue: number, targetIndex: number, isLength: boolean = false): Promise { + return new Promise(resolve => { + textRangePage.mapCitationLabelsToValues(label, index, citLabels, values).finally(() => { + expect(isLength ? values.length : values[targetIndex]).toBe(expectedValue); + return resolve(); + }); + }); + } + + const corpus: CorpusMC = new CorpusMC({citations: {4: new Citation({subcitations: {}, value: 4})}}); + textRangePage.corpusService.currentCorpus = new ReplaySubject(1); + textRangePage.corpusService.currentCorpus.next(corpus); + const valueList: number[] = []; + const citationLabels: string[] = ['4']; + let idx = 0; + const addReferencesSpy: Spy = spyOn(textRangePage, 'addReferences').and.returnValue(Promise.resolve()); + expectValueList('4', idx, [], valueList, 4, 0).then(async () => { + addReferencesSpy.and.callFake(() => Promise.reject()); + idx++; + await expectValueList('2', idx, citationLabels, valueList, 1, 0, true); + addReferencesSpy.and.callFake(() => { + return new Promise(resolve => { + corpus.citations['4'].subcitations['2'] = new Citation({value: 2}); + return resolve(); + }); + }); + await expectValueList('2', idx, citationLabels, valueList, 2, 1); + await expectValueList('2', idx, citationLabels, valueList, 2, 2); + corpus.citations['4'].subcitations = {}; + citationLabels.push('2'); + idx++; + await expectValueList('0', idx, citationLabels, valueList, 3, 0, true); + corpus.citations['4'].subcitations['2'] = new Citation({subcitations: {}}); + addReferencesSpy.and.callFake(() => Promise.reject()); + await expectValueList('3', idx, citationLabels, valueList, 3, 0, true); + addReferencesSpy.and.callFake(() => { + return new Promise(resolve => { + corpus.citations['4'].subcitations['2'].subcitations['3'] = new Citation({value: 3}); + return resolve(); + }); + }); + await expectValueList('3', idx, citationLabels, valueList, 3, 3); + await expectValueList('3', idx, citationLabels, valueList, 5, 0, true); + corpus.citations['4'].subcitations['2'].subcitations = {0: new Citation()}; + await expectValueList('3', idx, citationLabels, valueList, 5, 0, true); + done(); + }); + }); + + it('should be initialized', (done) => { + const addReferencesSpy: Spy = spyOn(textRangePage, 'addReferences').and.callFake(() => Promise.reject()); + const initPageSpy: Spy = spyOn(textRangePage, 'initPage').and.returnValue(Promise.resolve()); + textRangePage.corpusService.currentCorpus = new ReplaySubject(1); + textRangePage.corpusService.currentCorpus.next(new CorpusMC({citations: {}})); + textRangePage.ngOnInit().then(async () => { + expect(initPageSpy).toHaveBeenCalledTimes(0); + addReferencesSpy.and.returnValue(Promise.resolve()); + await textRangePage.ngOnInit(); + expect(initPageSpy).toHaveBeenCalledTimes(1); + textRangePage.corpusService.currentCorpus.next(new CorpusMC({citations: {1: new Citation()}})); + await textRangePage.ngOnInit(); + expect(initPageSpy).toHaveBeenCalledTimes(2); + done(); + }); + }); + + it('should reset citations', (done) => { + function expectTextRange(currentInputId: number, tr: TextRange, isStart: boolean, idx: number, + expectedValue: string): Promise { + return new Promise(resolve => { + textRangePage.currentInputId = currentInputId; + textRangePage.resetCitations().then(() => { + const target: string[] = isStart ? tr.start : tr.end; + expect(target[idx]).toBe(expectedValue); + return resolve(); + }); + }); + } + + textRangePage.corpusService.initCurrentTextRange(); + textRangePage.helperService.applicationState.next(textRangePage.helperService.deepCopy(MockMC.applicationState)); + textRangePage.corpusService.currentCorpus = new ReplaySubject(1); + textRangePage.corpusService.currentCorpus.next(new CorpusMC({citations: {1: new Citation()}})); + textRangePage.resetCitations().then(() => { + textRangePage.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { + expect(tr.start[1]).toBeTruthy(); + expectTextRange(1, tr, true, 1, '').then(() => { + tr.start[2] = '2'; + expectTextRange(2, tr, true, 2, '').then(() => { + expectTextRange(4, tr, false, 2, '').then(() => { + tr.end[2] = '2'; + expectTextRange(5, tr, false, 2, '').then(() => { + done(); + }); + }); + }); + }); + }); + }); + }); + + it('should reset the current input ID', (done) => { + textRangePage.currentInputId = 1; + textRangePage.resetCurrentInputId().then(() => { + expect(textRangePage.currentInputId).toBe(0); + done(); + }); + }); + + it('should show further references', (done) => { + const addReferencesSpy: Spy = spyOn(textRangePage, 'addReferences').and.returnValue(Promise.resolve()); + const addLvl3Spy: Spy = spyOn(textRangePage, 'addLevel3References').and.returnValue(Promise.resolve()); + textRangePage.corpusService.initCurrentTextRange(); + textRangePage.helperService.applicationState.next(textRangePage.helperService.deepCopy(MockMC.applicationState)); + textRangePage.corpusService.currentCorpus = new ReplaySubject(1); + textRangePage.corpusService.currentCorpus.next(new CorpusMC({ + citations: {2: new Citation({subcitations: {2: new Citation()}})} + })); + textRangePage.showFurtherReferences(true).then(async () => { + expect(addReferencesSpy).toHaveBeenCalledTimes(1); + textRangePage.corpusService.setCurrentTextRange(0, null, new TextRange({end: [''], start: ['']})); + await textRangePage.showFurtherReferences(false); + expect(addLvl3Spy).toHaveBeenCalledTimes(1); + textRangePage.corpusService.setCurrentTextRange(0, null, new TextRange({end: ['2'], start: ['2']})); + textRangePage.corpusService.currentCorpus.next(new CorpusMC({ + citations: {2: new Citation({subcitations: {}})}, + citation_level_2: CitationLevel.default.toString() + })); + await textRangePage.showFurtherReferences(false); + expect(addReferencesSpy).toHaveBeenCalledTimes(1); + expect(addLvl3Spy).toHaveBeenCalledTimes(2); + done(); + }); + }); }); diff --git a/src/app/text-range/text-range.page.ts b/src/app/text-range/text-range.page.ts index a71a849..c2a7cd2 100644 --- a/src/app/text-range/text-range.page.ts +++ b/src/app/text-range/text-range.page.ts @@ -39,7 +39,26 @@ export class TextRangePage implements OnInit { public helperService: HelperService) { } - addMissingCitations(citationLabelsStart: string[], citationLabelsEnd: string[]) { + addLevel3References(relevantTextRangePart: string[], currentCorpus: CorpusMC): Promise { + return new Promise(resolve => { + if ([2, 3, 5, 6].indexOf(this.currentInputId) > -1) { + const baseCit: Citation = currentCorpus.citations[relevantTextRangePart[0]]; + const relCit: Citation = baseCit.subcitations[relevantTextRangePart[1]]; + const hasLvl3: boolean = currentCorpus.citation_level_3 !== CitationLevel[CitationLevel.default]; + if (relevantTextRangePart[1] && !(relCit && Object.keys(relCit.subcitations).length) && hasLvl3) { + this.addReferences(currentCorpus.citation_level_3, [baseCit, relCit]).then(() => { + return resolve(); + }); + } else { + return resolve(); + } + } else { + return resolve(); + } + }); + } + + addMissingCitations(citationLabelsStart: string[], citationLabelsEnd: string[]): Promise { return new Promise((resolve, reject) => { this.mapCitationLabelsToValues(citationLabelsStart[0], 0, citationLabelsStart, this.citationValuesStart).then(() => { this.mapCitationLabelsToValues(citationLabelsEnd[0], 0, citationLabelsEnd, this.citationValuesEnd).then(() => { @@ -64,7 +83,7 @@ export class TextRangePage implements OnInit { return resolve(); } }); - }, () => reject()); + }); }); } @@ -76,9 +95,8 @@ export class TextRangePage implements OnInit { const urnLastPart: string = relevantCitations.map(x => x.isNumeric ? x.value.toString() : x.label).join('.'); this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { const fullUrn: string = cc.source_urn + (urnLastPart ? ':' + urnLastPart : ''); - this.corpusService.getCTSvalidReff(fullUrn).then((result: string[]) => { + this.corpusService.getCTSvalidReff(fullUrn).then((urnList: string[]) => { const newCitations: Citation[] = []; - const urnList: string[] = result as string[]; const replaceString: string = fullUrn + (urnLastPart ? '.' : ':'); urnList.forEach((urn) => { const urnModified: string = urn.replace(replaceString, ''); @@ -107,8 +125,8 @@ export class TextRangePage implements OnInit { const secondLabel: string = cc.citations[rc0Label].subcitations[rc1Label].label; this.currentlyAvailableCitations.push(firstLabel.concat('.', secondLabel, '.', citation.label)); } - return resolve(); }); + return resolve(); }, async (error: HttpErrorResponse) => { return reject(error); }); @@ -116,42 +134,48 @@ export class TextRangePage implements OnInit { }); } - applyAutoComplete(isStart: boolean) { - this.showFurtherReferences(isStart).then(() => { - const oldId: number = this.currentInputId; - this.currentInputId = 0; - let nextIdx = oldId; - let newEl: HTMLInputElement = null; - while (nextIdx < Math.min(oldId + 4, 6) && !newEl) { - nextIdx++; - const newId: string = 'input' + nextIdx.toString(); - newEl = document.getElementById(newId) as HTMLInputElement; - } - if (newEl) { - // adjust disabled state manually because the focus won't work otherwise and the automatic check comes too late - newEl.disabled = false; - newEl.focus(); - } + applyAutoComplete(isStart: boolean): Promise { + return new Promise(resolve => { + this.showFurtherReferences(isStart).then(() => { + const oldId: number = this.currentInputId; + this.currentInputId = 0; + let nextIdx = oldId; + let newEl: HTMLInputElement = null; + while (nextIdx < Math.min(oldId + 4, 6) && !newEl) { + nextIdx++; + const newId: string = 'input' + nextIdx.toString(); + newEl = document.getElementById(newId) as HTMLInputElement; + } + if (newEl) { + // adjust disabled state manually because the focus won't work otherwise and the automatic check comes too late + newEl.disabled = false; + newEl.focus(); + } + return resolve(); + }); }); } - checkInputDisabled(): void { - Object.keys(this.isInputDisabled).forEach((isStart: string) => { + checkInputDisabled(): Promise { + return new Promise(resolve => { this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { this.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { - const baseCits: { [label: string]: Citation } = cc.citations; - const ctrPart: string[] = +isStart ? tr.start : tr.end; - if (!baseCits.hasOwnProperty(ctrPart[0])) { - this.isInputDisabled[+isStart].next(true); - } else { - this.isInputDisabled[+isStart].next(!baseCits[ctrPart[0]].subcitations.hasOwnProperty(ctrPart[1])); - } + Object.keys(this.isInputDisabled).forEach((isStart: string) => { + const baseCits: { [label: string]: Citation } = cc.citations; + const ctrPart: string[] = +isStart ? tr.start : tr.end; + if (!baseCits.hasOwnProperty(ctrPart[0])) { + this.isInputDisabled[+isStart].next(true); + } else { + this.isInputDisabled[+isStart].next(!baseCits[ctrPart[0]].subcitations.hasOwnProperty(ctrPart[1])); + } + }); + return resolve(); }); }); }); } - checkTextRange(citationLabelsStart: string[], citationLabelsEnd: string[]) { + checkTextRange(citationLabelsStart: string[], citationLabelsEnd: string[]): Promise { return new Promise(resolve => { citationLabelsStart = citationLabelsStart.filter(x => x); citationLabelsEnd = citationLabelsEnd.filter(x => x); @@ -191,67 +215,71 @@ export class TextRangePage implements OnInit { const citationValuesStart: number[] = this.citationValuesStart; const citationValuesEnd: number[] = this.citationValuesEnd; if (citationValuesStart[0] < citationValuesEnd[0]) { - resolve(true); + return resolve(true); } else if (citationValuesStart.concat(citationValuesEnd).some(x => isNaN(x))) { // there are non-numeric citation values involved, so we cannot easily compare them - resolve(true); + return resolve(true); } else if (citationValuesStart[0] === citationValuesEnd[0]) { if (citationValuesStart.length > 1) { if (citationValuesStart[1] < citationValuesEnd[1]) { - resolve(true); + return resolve(true); } else if (this.citationValuesStart[1] === citationValuesEnd[1]) { if (citationValuesStart.length > 2) { - resolve(citationValuesStart[2] <= citationValuesEnd[2]); + return resolve(citationValuesStart[2] <= citationValuesEnd[2]); } else { - resolve(true); + return resolve(true); } } else { - resolve(false); + return resolve(false); } } else { - resolve(true); + return resolve(true); } } else { - resolve(false); + return resolve(false); } }); } - confirmSelection(skipText: boolean = false) { - if (this.isTextRangeCheckRunning) { - return; - } - this.isTextRangeCheckRunning = true; - this.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { - const citationLabelsStart: string[] = tr.start; - const citationLabelsEnd: string[] = tr.end; - this.checkTextRange(citationLabelsStart, citationLabelsEnd).then(async (isTextRangeCorrect: boolean) => { - this.isTextRangeCheckRunning = false; - if (!isTextRangeCorrect) { - this.helperService.showToast(this.toastCtrl, this.corpusService.invalidTextRangeString).then(); - return; - } - this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { - const newUrnBase: string = cc.source_urn + ':'; - if (this.citationValuesStart.concat(this.citationValuesEnd).some(x => isNaN(x))) { - this.corpusService.currentUrn = newUrnBase + tr.start.filter(x => x).join('.') + '-' + - tr.end.filter(x => x).join('.'); - } else { - this.corpusService.currentUrn = newUrnBase + this.citationValuesStart.join('.') + '-' + - this.citationValuesEnd.join('.'); + confirmSelection(skipText: boolean = false): Promise { + return new Promise((resolve) => { + if (this.isTextRangeCheckRunning) { + return resolve(); + } + this.isTextRangeCheckRunning = true; + this.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { + const citationLabelsStart: string[] = tr.start; + const citationLabelsEnd: string[] = tr.end; + this.checkTextRange(citationLabelsStart, citationLabelsEnd).then((isTextRangeCorrect: boolean) => { + this.isTextRangeCheckRunning = false; + if (!isTextRangeCorrect) { + this.helperService.showToast(this.toastCtrl, this.corpusService.invalidTextRangeString).then(); + return resolve(); } - this.helperService.applicationState.pipe(take(1)).subscribe((state: ApplicationState) => { - state.currentSetup.currentTextRange = tr; - this.corpusService.isTextRangeCorrect = true; - this.corpusService.getText().then(() => { - if (skipText) { - this.helperService.goToExerciseParametersPage(this.navCtrl).then(); - } else if (this.helperService.isVocabularyCheck) { - this.helperService.goToVocabularyCheckPage(this.navCtrl).then(); - } else { - this.helperService.goToShowTextPage(this.navCtrl).then(); - } - }, () => { + this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { + const newUrnBase: string = cc.source_urn + ':'; + if (this.citationValuesStart.concat(this.citationValuesEnd).some(x => isNaN(x))) { + this.corpusService.currentUrn = newUrnBase + tr.start.filter(x => x).join('.') + '-' + + tr.end.filter(x => x).join('.'); + } else { + this.corpusService.currentUrn = newUrnBase + this.citationValuesStart.join('.') + '-' + + this.citationValuesEnd.join('.'); + } + this.helperService.applicationState.pipe(take(1)).subscribe((state: ApplicationState) => { + state.currentSetup.currentTextRange = tr; + this.corpusService.isTextRangeCorrect = true; + this.corpusService.getText().then(() => { + if (skipText) { + this.helperService.goToExerciseParametersPage(this.navCtrl).then(); + } else if (this.helperService.isVocabularyCheck) { + this.helperService.goToVocabularyCheckPage(this.navCtrl).then(); + } else { + this.helperService.goToShowTextPage(this.navCtrl).then(); + } + return resolve(); + }, () => { + return resolve(); + }); }); }); }); @@ -259,17 +287,21 @@ export class TextRangePage implements OnInit { }); } - initPage(currentCorpus: CorpusMC) { - if (currentCorpus.citation_level_2 === CitationLevel[CitationLevel.default]) { - const firstKey: string = Object.keys(currentCorpus.citations)[0]; - const randomLabel: string = currentCorpus.citations[firstKey].label; - this.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { - tr.start[0] = tr.end[0] = randomLabel; - }); - } + initPage(currentCorpus: CorpusMC): Promise { + return new Promise(resolve => { + if (currentCorpus.citation_level_2 === CitationLevel[CitationLevel.default]) { + const firstKey: string = Object.keys(currentCorpus.citations)[0]; + const randomLabel: string = currentCorpus.citations[firstKey].label; + this.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { + tr.start[0] = tr.end[0] = randomLabel; + return resolve(); + }); + } + return resolve(); + }); } - public mapCitationLabelsToValues(label: string, index: number, citationLabels: string[], valueList: number[]) { + public mapCitationLabelsToValues(label: string, index: number, citationLabels: string[], valueList: number[]): Promise { return new Promise((resolve, reject) => { this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { if (index === 0 && cc.citations[label]) { @@ -328,98 +360,101 @@ export class TextRangePage implements OnInit { }); } - ngOnInit(): void { - this.currentlyAvailableCitations = []; - this.corpusService.isTextRangeCorrect = false; - this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { - if (Object.keys(cc.citations).length === 0) { - this.addReferences(cc.citation_level_1).then(() => { - this.initPage(cc); - }, () => { - }); - } else { - this.initPage(cc); - } + ngOnInit(): Promise { + return new Promise(resolve => { + this.currentlyAvailableCitations = []; + this.corpusService.isTextRangeCorrect = false; + this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { + if (Object.keys(cc.citations).length === 0) { + this.addReferences(cc.citation_level_1).then(() => { + this.initPage(cc).then(() => { + return resolve(); + }); + }, () => { + return resolve(); + }); + } else { + this.initPage(cc).then(() => { + return resolve(); + }); + } + }); }); } - resetCitations() { - this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { - this.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { - switch (this.currentInputId) { - case 1: - if (cc.citation_level_2 !== CitationLevel[CitationLevel.default]) { - tr.start[1] = ''; - tr.start[2] = ''; - } - break; - case 2: - if (cc.citation_level_3 !== CitationLevel[CitationLevel.default]) { - tr.start[2] = ''; - } - break; - case 4: - if (cc.citation_level_2 !== CitationLevel[CitationLevel.default]) { - tr.end[1] = ''; - tr.end[2] = ''; - } - break; - case 5: - if (cc.citation_level_3 !== CitationLevel[CitationLevel.default]) { - tr.end[2] = ''; - } - break; - default: - break; - } + resetCitations(): Promise { + return new Promise(resolve => { + this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { + this.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { + switch (this.currentInputId) { + case 1: + if (cc.citation_level_2 !== CitationLevel[CitationLevel.default]) { + tr.start[1] = ''; + tr.start[2] = ''; + } + break; + case 2: + if (cc.citation_level_3 !== CitationLevel[CitationLevel.default]) { + tr.start[2] = ''; + } + break; + case 4: + if (cc.citation_level_2 !== CitationLevel[CitationLevel.default]) { + tr.end[1] = ''; + tr.end[2] = ''; + } + break; + case 5: + if (cc.citation_level_3 !== CitationLevel[CitationLevel.default]) { + tr.end[2] = ''; + } + break; + default: + break; + } + }); + return resolve(); }); }); } - resetCurrentInputId() { - const oldId: number = this.currentInputId; - // dirty hack to prevent the blur event from triggering before the click event - setTimeout(() => { - if (oldId === this.currentInputId) { - this.currentInputId = 0; - } - }, 50); + resetCurrentInputId(): Promise { + return new Promise(resolve => { + const oldId: number = this.currentInputId; + // dirty hack to prevent the blur event from triggering before the click event + setTimeout(() => { + if (oldId === this.currentInputId) { + this.currentInputId = 0; + } + return resolve(); + }, 50); + }); } - async showFurtherReferences(isStart: boolean): Promise { - return new Promise(outerResolve => { + showFurtherReferences(isStart: boolean): Promise { + return new Promise(resolve => { this.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { const relTextRangePart: string[] = isStart ? tr.start : tr.end; - this.resetCitations(); - this.checkInputDisabled(); - if (!relTextRangePart[0]) { - return outerResolve(); - } - this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { - new Promise(innerResolve => { - const baseCit: Citation = cc.citations[relTextRangePart[0]]; - if (baseCit && (Object.keys(baseCit.subcitations).length || - cc.citation_level_2 === CitationLevel[CitationLevel.default])) { - innerResolve(); - return outerResolve(); - } else { - this.addReferences(cc.citation_level_2, [baseCit]).then(() => { - innerResolve(); - return outerResolve(); - }, () => { - innerResolve(); - return outerResolve(); - }); + this.resetCitations().then(() => { + this.checkInputDisabled().then(() => { + if (!relTextRangePart[0]) { + return resolve(); } - }).then(() => { - if ([2, 3, 5, 6].indexOf(this.currentInputId) > -1) { + this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { const baseCit: Citation = cc.citations[relTextRangePart[0]]; - const relCit: Citation = baseCit.subcitations[relTextRangePart[1]]; - const hasLvl3: boolean = cc.citation_level_3 !== CitationLevel[CitationLevel.default]; - if (relTextRangePart[1] && !(relCit && Object.keys(relCit.subcitations).length) && hasLvl3) { - this.addReferences(cc.citation_level_3, [baseCit, relCit]).then(); + if (baseCit && (Object.keys(baseCit.subcitations).length || + cc.citation_level_2 === CitationLevel[CitationLevel.default])) { + this.addLevel3References(relTextRangePart, cc).then(() => { + return resolve(); + }); + } else { + this.addReferences(cc.citation_level_2, [baseCit]).finally(() => { + this.addLevel3References(relTextRangePart, cc).then(() => { + return resolve(); + }); + }); } - } + }); }); }); }); diff --git a/src/app/translate-testing/translate-testing.module.ts b/src/app/translate-testing/translate-testing.module.ts index 9b6e26c..f82b8f7 100644 --- a/src/app/translate-testing/translate-testing.module.ts +++ b/src/app/translate-testing/translate-testing.module.ts @@ -1,6 +1,6 @@ import {EventEmitter, Injectable, NgModule, Pipe, PipeTransform} from '@angular/core'; import {TranslateLoader, TranslateModule, TranslatePipe, TranslateService} from '@ngx-translate/core'; -import {Observable, of, Subscriber} from 'rxjs'; +import {Observable, of} from 'rxjs'; import { DefaultLangChangeEvent, LangChangeEvent, @@ -10,7 +10,7 @@ import { const translations: any = {}; -class FakeLoader implements TranslateLoader { +export class FakeLoader implements TranslateLoader { getTranslation(lang: string): Observable { return of(translations); } @@ -38,9 +38,6 @@ export class TranslateServiceStub { return of(key); } - public addLangs(langs: string[]) { - } - public setDefaultLang(lang: string) { } diff --git a/src/app/vocabulary-check/vocabulary-check.page.spec.ts b/src/app/vocabulary-check/vocabulary-check.page.spec.ts index 297191e..db2de5b 100644 --- a/src/app/vocabulary-check/vocabulary-check.page.spec.ts +++ b/src/app/vocabulary-check/vocabulary-check.page.spec.ts @@ -8,9 +8,12 @@ import {RouterModule} from '@angular/router'; import {TranslateTestingModule} from '../translate-testing/translate-testing.module'; import {FormsModule} from '@angular/forms'; import {APP_BASE_HREF} from '@angular/common'; +import MockMC from '../models/mockMC'; +import Spy = jasmine.Spy; +import {Sentence} from '../models/sentence'; describe('VocabularyCheckPage', () => { - let component: VocabularyCheckPage; + let vocabularyCheckPage: VocabularyCheckPage; let fixture: ComponentFixture; beforeEach(async(() => { @@ -27,17 +30,58 @@ describe('VocabularyCheckPage', () => { {provide: APP_BASE_HREF, useValue: '/'}, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], - }) - .compileComponents(); + }).compileComponents().then(); })); beforeEach(() => { fixture = TestBed.createComponent(VocabularyCheckPage); - component = fixture.componentInstance; + vocabularyCheckPage = fixture.componentInstance; + vocabularyCheckPage.vocService.ngOnInit(); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should create', (done) => { + expect(vocabularyCheckPage).toBeTruthy(); + expect(vocabularyCheckPage.filterArray(['', 'a']).length).toBe(1); + const navSpy: Spy = spyOn(vocabularyCheckPage.helperService, 'goToAuthorPage').and.returnValue(Promise.resolve(true)); + vocabularyCheckPage.chooseCorpus().then(() => { + expect(navSpy).toHaveBeenCalledTimes(1); + done(); + }); + }); + + it('should check vocabulary', (done) => { + vocabularyCheckPage.corpusService.initCurrentCorpus(); + vocabularyCheckPage.corpusService.initCurrentTextRange(); + vocabularyCheckPage.helperService.applicationState.next(vocabularyCheckPage.helperService.deepCopy(MockMC.applicationState)); + vocabularyCheckPage.vocService.frequencyUpperBound = -1; + const getVocSpy: Spy = spyOn(vocabularyCheckPage.vocService, 'getVocabularyCheck') + .and.returnValue(Promise.resolve([])); + const navSpy: Spy = spyOn(vocabularyCheckPage.helperService, 'goToRankingPage').and.returnValue(Promise.resolve(true)); + vocabularyCheckPage.checkVocabulary().then(async () => { + expect(getVocSpy).toHaveBeenCalledTimes(0); + vocabularyCheckPage.vocService.frequencyUpperBound = 500; + vocabularyCheckPage.corpusService.isTextRangeCorrect = false; + await vocabularyCheckPage.checkVocabulary(); + expect(getVocSpy).toHaveBeenCalledTimes(0); + vocabularyCheckPage.corpusService.isTextRangeCorrect = true; + await vocabularyCheckPage.checkVocabulary(); + expect(navSpy).toHaveBeenCalledTimes(1); + getVocSpy.and.callFake(() => Promise.reject()); + await vocabularyCheckPage.checkVocabulary(); + expect(navSpy).toHaveBeenCalledTimes(1); + done(); + }); + }); + + it('should process sentences', () => { + vocabularyCheckPage.currentRankingUnits = []; + const sentences: Sentence[] = Array(50).fill(null).map(x => new Sentence({matching_degree: 40})); + sentences[0].matching_degree = 49; + sentences[10].matching_degree = 50; + sentences[14].matching_degree = 80; + sentences[25].matching_degree = 5; + vocabularyCheckPage.processSentences(sentences); + expect(vocabularyCheckPage.currentRankingUnits.length).toBe(5); }); }); diff --git a/src/app/vocabulary-check/vocabulary-check.page.ts b/src/app/vocabulary-check/vocabulary-check.page.ts index 4462abc..605b573 100644 --- a/src/app/vocabulary-check/vocabulary-check.page.ts +++ b/src/app/vocabulary-check/vocabulary-check.page.ts @@ -18,13 +18,11 @@ import {TextRange} from '../models/textRange'; styleUrls: ['./vocabulary-check.page.scss'], }) export class VocabularyCheckPage { - invalidSentenceCountString: string; ObjectKeys = Object.keys; VocabularyCorpus = VocabularyCorpus; VocabularyCorpusTranslation = VocabularyCorpusTranslation; public adaptPassages = true; public currentRankingUnits: Sentence[][]; - public invalidQueryCorpusString: string; constructor(public navCtrl: NavController, public vocService: VocabularyService, @@ -34,43 +32,45 @@ export class VocabularyCheckPage { public http: HttpClient, public exerciseService: ExerciseService, public helperService: HelperService) { - this.translate.get('INVALID_SENTENCE_COUNT').subscribe(value => this.invalidSentenceCountString = value); - this.translate.get('INVALID_QUERY_CORPUS').subscribe(value => this.invalidQueryCorpusString = value); } - checkVocabulary() { - this.corpusService.currentCorpus.pipe(take(1)).subscribe(async (cc: CorpusMC) => { - this.corpusService.currentTextRange.pipe(take(1)).subscribe(async (tr: TextRange) => { - if (this.vocService.desiredSentenceCount < 0 || this.vocService.frequencyUpperBound < 0) { - this.helperService.showToast(this.toastCtrl, this.invalidSentenceCountString).then(); - return; - } else if (!cc || tr.start.length === 0 || tr.end.length === 0 || !this.corpusService.isTextRangeCorrect) { - this.helperService.showToast(this.toastCtrl, this.invalidQueryCorpusString).then(); - return; - } - this.vocService.currentSentences = []; - this.currentRankingUnits = []; - this.vocService.ranking = []; - // remove old sentence boundaries - this.corpusService.currentUrn = this.corpusService.currentUrn.split('@')[0]; - this.vocService.getVocabularyCheck(this.corpusService.currentUrn, false).then((sentences: Sentence[]) => { - this.processSentences(sentences); - this.navCtrl.navigateForward('/ranking').then(); - }, async (error: HttpErrorResponse) => { + checkVocabulary(): Promise { + return new Promise((resolve) => { + this.corpusService.currentCorpus.pipe(take(1)).subscribe(async (cc: CorpusMC) => { + this.corpusService.currentTextRange.pipe(take(1)).subscribe(async (tr: TextRange) => { + if (this.vocService.desiredSentenceCount < 0 || this.vocService.frequencyUpperBound < 0) { + this.helperService.showToast(this.toastCtrl, this.corpusService.invalidSentenceCountString).then(); + return resolve(); + } else if (!cc || tr.start.length === 0 || tr.end.length === 0 || !this.corpusService.isTextRangeCorrect) { + this.helperService.showToast(this.toastCtrl, this.corpusService.invalidQueryCorpusString).then(); + return resolve(); + } + this.vocService.currentSentences = []; + this.currentRankingUnits = []; + this.vocService.ranking = []; + // remove old sentence boundaries + this.corpusService.currentUrn = this.corpusService.currentUrn.split('@')[0]; + this.vocService.getVocabularyCheck(this.corpusService.currentUrn, false).then((sentences: Sentence[]) => { + this.processSentences(sentences); + this.helperService.goToRankingPage(this.navCtrl).then(); + return resolve(); + }, async (error: HttpErrorResponse) => { + return resolve(); + }); }); }); }); } - chooseCorpus() { - this.navCtrl.navigateForward('/author').then(); + chooseCorpus(): Promise { + return this.helperService.goToAuthorPage(this.navCtrl); } filterArray(array: string[]): string[] { return array.filter(x => x); } - processSentences(sentences: Sentence[]) { + processSentences(sentences: Sentence[]): void { if (sentences.length > this.vocService.desiredSentenceCount) { this.currentRankingUnits.push([]); sentences.forEach((sent: Sentence) => { diff --git a/src/app/vocabulary.service.spec.ts b/src/app/vocabulary.service.spec.ts index 1b71b29..c7e446d 100644 --- a/src/app/vocabulary.service.spec.ts +++ b/src/app/vocabulary.service.spec.ts @@ -2,7 +2,13 @@ import {TestBed} from '@angular/core/testing'; import {VocabularyService} from './vocabulary.service'; import {HttpClientTestingModule} from '@angular/common/http/testing'; -import {HelperService} from './helper.service'; +import {IonicStorageModule} from '@ionic/storage'; +import {TranslateTestingModule} from './translate-testing/translate-testing.module'; +import {VocabularyCorpus} from './models/enum'; +import {Sentence} from './models/sentence'; +import {AnnisResponse} from './models/annisResponse'; +import {HttpErrorResponse} from '@angular/common/http'; +import Spy = jasmine.Spy; describe('VocabularyService', () => { let vocabularyService: VocabularyService; @@ -10,10 +16,10 @@ describe('VocabularyService', () => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule, + IonicStorageModule.forRoot(), + TranslateTestingModule, ], - providers: [ - {provide: HelperService, useValue: {}}, - ], + providers: [], }); vocabularyService = TestBed.inject(VocabularyService); } @@ -21,5 +27,40 @@ describe('VocabularyService', () => { it('should be created', () => { expect(vocabularyService).toBeTruthy(); + const sentences: Sentence[] = [new Sentence({matching_degree: 3}), new Sentence({matching_degree: 7})]; + expect(vocabularyService.getMean(sentences)).toBe(5); + }); + it('should get a vocabulary check', (done) => { + const error: HttpErrorResponse = new HttpErrorResponse({status: 500}); + const requestSpy: Spy = spyOn(vocabularyService.helperService, 'makeGetRequest') + .and.callFake(() => Promise.reject(error)); + // result: AnnisResponse | Sentence[] + vocabularyService.getVocabularyCheck('', false).then(() => { + }, async (response: HttpErrorResponse) => { + expect(response.status).toBe(500); + requestSpy.and.returnValue(Promise.resolve(new AnnisResponse())); + const result: AnnisResponse | Sentence[] = await vocabularyService.getVocabularyCheck('', true); + expect(result.hasOwnProperty('length')).toBe(false); + done(); + }); + }); + + it('should be initialized', () => { + vocabularyService.ngOnInit(); + expect(Object.keys(vocabularyService.refVocMap).length).toBe(4); + // vocabularyService.currentReferenceVocabulary = VocabularyCorpus.bws; + expect(vocabularyService.getCurrentReferenceVocabulary().totalCount).toBe(1276); + expect(vocabularyService.getPossibleSubCount()).toBe(500); + }); + + it('should update the reference range', () => { + vocabularyService.frequencyUpperBound = 0; + vocabularyService.ngOnInit(); + vocabularyService.currentReferenceVocabulary = VocabularyCorpus.agldt; + vocabularyService.updateReferenceRange(); + expect(vocabularyService.frequencyUpperBound).toBe(500); + vocabularyService.currentReferenceVocabulary = VocabularyCorpus.viva; + vocabularyService.updateReferenceRange(); + expect(vocabularyService.frequencyUpperBound).toBe(1164); }); }); diff --git a/src/app/vocabulary.service.ts b/src/app/vocabulary.service.ts index 6275868..f9af782 100644 --- a/src/app/vocabulary.service.ts +++ b/src/app/vocabulary.service.ts @@ -1,6 +1,6 @@ /* tslint:disable:no-string-literal */ import {HttpClient, HttpErrorResponse, HttpParams} from '@angular/common/http'; -import {Injectable} from '@angular/core'; +import {Injectable, OnInit} from '@angular/core'; import {VocabularyCorpus} from 'src/app/models/enum'; import {Vocabulary} from 'src/app/models/vocabulary'; import {Sentence} from 'src/app/models/sentence'; @@ -13,7 +13,7 @@ import configMC from '../configMC'; @Injectable({ providedIn: 'root' }) -export class VocabularyService { +export class VocabularyService implements OnInit { currentReferenceVocabulary: VocabularyCorpus = VocabularyCorpus.bws; currentSentences: Sentence[] = []; currentTestResults: { [exerciseIndex: number]: TestResultMC } = {}; @@ -25,33 +25,13 @@ export class VocabularyService { constructor(public http: HttpClient, public toastCtrl: ToastController, public helperService: HelperService) { - this.refVocMap[VocabularyCorpus.agldt] = new Vocabulary({ - hasFrequencyOrder: true, - totalCount: 7182, - possibleSubcounts: [] - }); - this.refVocMap[VocabularyCorpus.bws] = new Vocabulary({ - hasFrequencyOrder: false, - totalCount: 1276, - possibleSubcounts: [500, 1276] - }); - this.refVocMap[VocabularyCorpus.proiel] = new Vocabulary({ - hasFrequencyOrder: true, - totalCount: 16402, - possibleSubcounts: [] - }); - this.refVocMap[VocabularyCorpus.viva] = new Vocabulary({ - hasFrequencyOrder: false, - totalCount: 1164, - possibleSubcounts: [1164] - }); } getCurrentReferenceVocabulary(): Vocabulary { return this.refVocMap[this.currentReferenceVocabulary]; } - getMean(sentences: Sentence[]) { + getMean(sentences: Sentence[]): number { return sentences.map(x => x.matching_degree).reduce((a, b) => a + b) / sentences.length; } @@ -67,14 +47,37 @@ export class VocabularyService { .set('frequency_upper_bound', this.frequencyUpperBound.toString()) .set('query_urn', queryUrn) .set('show_oov', showOOV ? '1' : '0'); - this.helperService.makeGetRequest(this.http, this.toastCtrl, url, params).then((ar: AnnisResponse) => { - return resolve(ar); + this.helperService.makeGetRequest(this.http, this.toastCtrl, url, params).then((result: AnnisResponse | Sentence[]) => { + return resolve(result); }, (error: HttpErrorResponse) => { return reject(error); }); })); } + ngOnInit(): void { + this.refVocMap[VocabularyCorpus.agldt] = new Vocabulary({ + hasFrequencyOrder: true, + totalCount: 7182, + possibleSubcounts: [] + }); + this.refVocMap[VocabularyCorpus.bws] = new Vocabulary({ + hasFrequencyOrder: false, + totalCount: 1276, + possibleSubcounts: [500, 1276] + }); + this.refVocMap[VocabularyCorpus.proiel] = new Vocabulary({ + hasFrequencyOrder: true, + totalCount: 16402, + possibleSubcounts: [] + }); + this.refVocMap[VocabularyCorpus.viva] = new Vocabulary({ + hasFrequencyOrder: false, + totalCount: 1164, + possibleSubcounts: [1164] + }); + } + updateReferenceRange(): void { const hasFreq: boolean = this.getCurrentReferenceVocabulary().hasFrequencyOrder; this.frequencyUpperBound = hasFreq ? 500 : this.getPossibleSubCount(); diff --git a/src/configMC.ts b/src/configMC.ts index 6c13b28..d0e6416 100644 --- a/src/configMC.ts +++ b/src/configMC.ts @@ -41,6 +41,7 @@ export default { pageUrlInfo: '/info', pageUrlKwic: '/kwic', pageUrlPreview: '/preview', + pageUrlRanking: '/ranking', pageUrlShowText: '/show-text', pageUrlSemantics: '/semantics', pageUrlSources: '/sources', -- GitLab